← System Design Simulator

News Feed (Facebook / Instagram style) System Design Interview Question

By Rahul Kumar · Senior Software Engineer · Updated · 11 components · 4 operations ·Source: Alex Xu, System Design Interview Vol 1, Chapter 11

Problem: Design a news feed system that shows a user the latest posts from people they follow.

Overview

A news feed is the most-hit screen in a social app: roughly 300M daily active users open it ten times a day, which works out to ~35K feed reads per second at average and ~100K at peak. The problem is not rendering twenty posts — it is deciding whose twenty posts to render without scanning every post every followee has ever made. The book splits the system into two halves: feed publishing (a post is written and distributed into followers' timelines) and feed building (a user opens the app and reads a precomputed list). The interesting design pressure is the long-tailed follower distribution. A typical user has 200 followers; a celebrity has tens of millions. Naively pre-materializing timelines on every write means one celebrity post costs 100M writes, while materializing on read means every feed GET fans in across potentially thousands of authors. The hybrid model below is the book's answer to that asymmetry.

News Feed (Facebook / Instagram style) — Interactive Simulator

Runs fully client-side in your browser; no sign-up. Or open full screen →

Launch the interactive walkthrough for News Feed (Facebook / Instagram style) — animated architecture diagram, step-by-step flow with real payloads, component swap, and a discrete-event stress simulator.

Summary

A news feed system for ~300M DAU split into two halves the book defines explicitly: (1) feed publishing — a post service + fanout service that writes a new post and populates follower news-feed caches — and (2) news feed building — a news feed service that reads the precomputed cache and hydrates posts. The core tradeoff the book walks through is fan-out-on-write vs fan-out-on-read: pure on-write breaks on celebrity posts (a 100M-follower post = 100M writes), while pure on-read makes feed GET require N fan-ins of all followees' posts. The recommended solution is a hybrid — fan-out-on-write for regular users (precomputed timelines in Redis) and fan-out-on-read for celebrities with >1M followers, merged at read time for that small set. Backed by user/post/graph stores with aggressive caching (news feed cache, post cache, user cache), and a notification service that can ping followers about new content.

Requirements

Functional

Non-functional

Capacity Assumptions

Back-of-Envelope Estimates

High-level architecture

The write path starts at a post service that persists the post to a sharded Postgres/MySQL cluster (user_id as shard key), pushes the post_id onto a fanout queue (Kafka), and returns 202 immediately so the user sees their post instantly via optimistic UI. A fanout worker consumes the queue, looks up the author's followers via the user-graph service, and branches: if the author has fewer than 1M followers it iterates the follower list and LPUSHes the new post_id into each follower's Redis timeline (capped at ~1000 entries), which is cheap because the median author has ~200 followers. If the author is a celebrity (>1M followers), the worker skips fanout entirely and the author's posts are pulled at read time — this trades a tiny merge cost on every feed read for avoiding the pathological 100M-write burst. The read path is a news-feed service that, on GET /feed, first fetches the user's precomputed timeline IDs from Redis, then merges in any celebrity authors the user follows by querying their latest posts from a post-by-author index, then hydrates post and user details through a read-through post cache (Redis) and user cache, and finally returns a cursor-paginated page. Consistent hashing places each user's timeline and post shard together so fanout writes and feed reads stay local. The tradeoff is a hybrid code path that has to be tuned: getting the celebrity threshold wrong turns a clean design into either a write storm or a read storm.

Architecture Components (11)

Operations Walked Through (4)

Implementation

FeedItem model
package com.systemdesign.newsfeed.model;

import java.time.Instant;
import java.util.List;

public final class FeedItem {
    private final long postId;
    private final long authorId;
    private final String authorName;
    private final String text;
    private final List<String> mediaUrls;
    private final Instant createdAt;
    private final int likeCount;
    private final int commentCount;

    public FeedItem(long postId, long authorId, String authorName, String text,
                    List<String> mediaUrls, Instant createdAt, int likeCount, int commentCount) {
        this.postId = postId;
        this.authorId = authorId;
        this.authorName = authorName;
        this.text = text;
        this.mediaUrls = mediaUrls;
        this.createdAt = createdAt;
        this.likeCount = likeCount;
        this.commentCount = commentCount;
    }

    public long getPostId() { return postId; }
    public long getAuthorId() { return authorId; }
    public String getAuthorName() { return authorName; }
    public String getText() { return text; }
    public List<String> getMediaUrls() { return mediaUrls; }
    public Instant getCreatedAt() { return createdAt; }
    public int getLikeCount() { return likeCount; }
    public int getCommentCount() { return commentCount; }
}
FeedService — hybrid fanout (push for normal, pull for celebrities)
package com.systemdesign.newsfeed.service;

import com.systemdesign.newsfeed.model.FeedItem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

@Service
public class FeedService {

    private static final int CELEBRITY_THRESHOLD = 1_000_000;
    private static final int TIMELINE_CAP = 1000;

    private final TimelineCache timelineCache;        // Redis LIST per user
    private final UserGraphClient graph;              // followers / following
    private final PostRepository postRepo;            // sharded by author_id
    private final PostCache postCache;                // Redis HASH per post

    @Autowired
    public FeedService(TimelineCache timelineCache, UserGraphClient graph,
                       PostRepository postRepo, PostCache postCache) {
        this.timelineCache = timelineCache;
        this.graph = graph;
        this.postRepo = postRepo;
        this.postCache = postCache;
    }

    /** Write path: called from the fanout worker once per new post. */
    public void onNewPost(long authorId, long postId) {
        long followerCount = graph.followerCount(authorId);
        if (followerCount >= CELEBRITY_THRESHOLD) {
            return; // pull model on read
        }
        for (long followerId : graph.followers(authorId)) {
            timelineCache.prepend(followerId, postId, TIMELINE_CAP);
        }
    }

    /** Read path: return up to `limit` items older than `cursor`. */
    public List<FeedItem> buildFeed(long userId, long cursor, int limit) {
        List<Long> pushIds = timelineCache.range(userId, cursor, limit);
        List<Long> celebrityIds = new ArrayList<>();
        for (long authorId : graph.following(userId)) {
            if (graph.followerCount(authorId) >= CELEBRITY_THRESHOLD) {
                celebrityIds.addAll(postRepo.latestPostIds(authorId, cursor, limit));
            }
        }
        List<Long> merged = new ArrayList<>(pushIds);
        merged.addAll(celebrityIds);
        return merged.stream()
                .distinct()
                .map(postCache::hydrate)
                .sorted(Comparator.comparing(FeedItem::getCreatedAt).reversed())
                .limit(limit)
                .toList();
    }
}
FeedController — cursor-paginated REST endpoint
package com.systemdesign.newsfeed.api;

import com.systemdesign.newsfeed.model.FeedItem;
import com.systemdesign.newsfeed.service.FeedService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/v1/feed")
public class FeedController {

    private final FeedService feedService;

    public FeedController(FeedService feedService) {
        this.feedService = feedService;
    }

    @GetMapping
    public ResponseEntity<Map<String, Object>> getFeed(
            @AuthenticationPrincipal Long userId,
            @RequestParam(defaultValue = "0") long cursor,
            @RequestParam(defaultValue = "20") int limit) {
        if (limit > 50) limit = 50;
        List<FeedItem> items = feedService.buildFeed(userId, cursor, limit);
        long nextCursor = items.isEmpty()
                ? 0
                : items.get(items.size() - 1).getPostId();
        return ResponseEntity.ok(Map.of(
                "items", items,
                "nextCursor", nextCursor,
                "hasMore", items.size() == limit));
    }
}

Key design decisions & trade-offs

Interview follow-ups

Related