System Design Interviews, Step by Step · Classic Walkthroughs, Part 1

Design a News Feed

Show each user recent posts from the people they follow. The core decision is fan-out on write (precompute feeds) vs fan-out on read (build at request time), and the hybrid that handles celebrities.

Create a free account to save this page, take notes and track your progress. Sign in

The question

"Design the home feed of a social app: users follow others and see a reverse-chronological feed of their posts."

Clarify the requirements

  • Functional: create posts (text + image links); follow users; home feed of followed users' posts, newest first, paginated.
  • Scale: 200 million daily active users; on average 200 follows each; some accounts have 50 million followers; users post ~1 time/day and open the feed ~10 times/day.
  • Non-functional: feed loads in < 300 ms; a new post may take a few seconds to appear (eventual consistency is fine); feed must be highly available.
  • Out of scope: ranking by relevance (we'll mention it), ads, comments.

Estimate the scale

  • Posts: 200M/day ≈ 2,300 posts/s.
  • Feed reads: 2B/day ≈ 23,000 reads/s (peak ~70,000/s). Ten reads for every write: optimise reads.
  • Fan-out if we push each post to all followers: 2,300 posts/s × 200 followers ≈ 460,000 feed inserts/s, which is heavy but parallel and spread across workers.
  • One post from a 50M-follower account would mean 50 million inserts: the celebrity problem.

API and data model

POST /posts                 { text, image_url }
POST /follow/{user_id}
GET  /feed?cursor=...&limit=20   -> { posts: [...], next_cursor }
posts:   post_id (time-sortable, e.g. snowflake) | author_id | text | image_url | created_at
follows: follower_id | followee_id
feed cache (Redis): feed:{user_id} -> list of the latest ~800 post_ids

High-level design

            write path                                       read path
author ─► post service ─► posts DB              reader ─► feed service ─► feed cache (post ids)
                │                                                   │          └─► celebrity posts (pull)
                └─► queue ─► fan-out workers ─► feed cache            └─► post cache (hydrate ids -> posts)
                                   (followers lookup)
  • Write: store the post, then a queue hands it to fan-out workers that prepend the post ID to each follower's cached feed.
  • Read: read the user's list of post IDs, merge in recent posts from any celebrities they follow, then hydrate the IDs into full posts from a post cache.

Deep dives

1. Push vs pull.

Fan-out on write (push) Fan-out on read (pull)
When work happens When a post is created When a feed is opened
Read speed Fast: feed is precomputed Slow: query every followee, merge
Write cost High: one insert per follower Low
Celebrities 50M inserts per post Fine
Inactive users Wasted work No waste

The hybrid: push for normal accounts, pull for accounts above a follower threshold (say 100,000). Readers merge the two at read time.

Hybrid fan-out: push for most, pull for celebritiesTry it yourself
CELEBRITY_THRESHOLD = 3            # tiny for the demo; ~100,000 in real life

follows = {                        # follower -> followees
    "ana": ["ben", "cat", "star"],
    "ben": ["ana", "star"],
    "dan": ["ana", "ben", "cat", "star"],
}
followers = {}
for f, followees in follows.items():
    for e in followees:
        followers.setdefault(e, []).append(f)
followers["star"] += ["fan1", "fan2", "fan3"]   # star has 6 followers

feeds = {}                         # precomputed feed cache: user -> [post ids], newest first
posts_by_author = {}               # for pulling celebrity posts at read time
writes = 0


def publish(post_id, author):
    global writes
    posts_by_author.setdefault(author, []).insert(0, post_id)
    if len(followers.get(author, [])) >= CELEBRITY_THRESHOLD:
        return                      # celebrity: don't fan out, readers pull
    for f in followers.get(author, []):
        feeds.setdefault(f, []).insert(0, post_id)
        writes += 1


def read_feed(user, limit=5):
    pushed = feeds.get(user, [])
    pulled = [p for celeb in follows.get(user, []) if len(followers.get(celeb, [])) >= CELEBRITY_THRESHOLD
              for p in posts_by_author.get(celeb, [])]
    return sorted(set(pushed + pulled), reverse=True)[:limit]    # ids are time-sortable


for post_id, author in [(101, "ben"), (102, "star"), (103, "cat"), (104, "ana"), (105, "star")]:
    publish(post_id, author)

print("feed inserts written:", writes, "(star's 2 posts would have added 12)")
for user in ["ana", "ben", "dan"]:
    print(f"{user}'s feed:", read_feed(user))

Output

feed inserts written: 6 (star's 2 posts would have added 12)
ana's feed: [105, 103, 102, 101]
ben's feed: [105, 104, 102]
dan's feed: [105, 104, 103, 102, 101]

2. Pagination. Use a cursor (the last post ID seen) rather than page numbers: new posts at the top don't shift what the next page returns.

3. Feed cache size. Keep only the latest few hundred IDs per user; older pages fall back to a slower pull. Don't precompute feeds for users who haven't logged in for 30 days; build theirs on demand.

4. Ranking (if asked). Generate candidates (the chronological feed plus a few extras), score them with a model (likes, recency, relationship), and cache the ranked list briefly.

Trade-offs

  • Push vs pull vs hybrid: read latency vs write amplification.
  • Freshness: precomputed feeds are seconds behind; acceptable here, stated explicitly.
  • Storing IDs vs full posts in feeds: IDs keep the cache small and let edits and deletions show up via the post cache.
  • Deletes and unfollows: filter at read time (cheap) rather than purging every feed (expensive).

What interviewers look for

  • The push/pull trade-off and the celebrity problem, with a hybrid.
  • Separating the write path (async fan-out) from the read path (cache + hydrate).
  • Cursor pagination and time-sortable IDs.
  • Numbers that show why fan-out cost matters.

Key points

  • Push for normal users, pull for celebrities, merge at read time.
  • Fan-out runs asynchronously through a queue.
  • Feed caches hold post IDs; hydrate from a post cache.
  • Cursor-based pagination; skip precomputing feeds for inactive users.

Practice questions

What is the celebrity problem in news feed design, and how is it usually solved?

Answer

With fan-out on write, one post from an account with millions of followers triggers millions of feed inserts. The usual fix is a hybrid: push posts from normal accounts into followers' feeds, but don't fan out celebrity posts; readers pull those at read time and merge them in.

Why use cursor-based pagination for feeds instead of page numbers?

Answer

New posts keep arriving at the top. With page numbers, items shift between requests, causing duplicates or skipped posts. A cursor (the last post ID seen) asks for 'posts older than this', which stays stable.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?