Back to Case Studies
system-designadvanced 13 min read

System Design Interview

Design a Social News Feed (Twitter / LinkedIn)

Fan-out on write vs fan-out on read — the core trade-off that shapes every social feed architecture

Key outcome: Feed render in <100ms for 500M users
System DesignFeedFan-outRedisCassandraRanking

The Interview Question

"Design the news feed for a social platform. When a user posts, their followers see it in their feed. The feed should be roughly chronological and personalised."

This is one of the most discussed system design questions because the core trade-off — fan-out on write vs fan-out on read — is genuinely hard to get right at scale, and the right answer depends on the follower distribution of your user base.


Step 1: Requirements

Functional

  • Users follow other users
  • When a user posts, all followers eventually see the post in their feed
  • Feed is ordered: most recent first (with optional ranking)
  • Feed should load in under 300ms

Non-functional

  • 500 million daily active users
  • 50 million posts per day (~580 posts/second)
  • 500 million feed reads per day (~5,800/second)
  • Some users have 50 million+ followers (celebrities, news accounts)
  • Some users follow 5,000+ accounts

Step 2: The Core Trade-Off

Fan-out on write (push model): When User A posts, immediately write that post into every follower's feed timeline cache.

A posts → find all N followers → write post_id to timeline:follower_1
                                → write post_id to timeline:follower_2
                                → ...
                                → write post_id to timeline:follower_N
Feed read → read from pre-built timeline → O(1), instant

Fan-out on read (pull model): When a user opens their feed, query the database for recent posts from everyone they follow.

A posts → write post to posts table (one write)
Feed read → find all accounts I follow
          → for each: fetch their recent posts
          → merge and sort → return feed

Why neither works alone:

| | Fan-out on Write | Fan-out on Read | |--|--|--| | Write cost | O(N followers) per post | O(1) per post | | Read cost | O(1) — pre-built | O(K followees) per read | | Celebrity problem | Writing to 50M timelines takes minutes | Acceptable | | Heavy follower problem | Acceptable | Users following 5K accounts: 5K DB reads per feed load |


Step 3: The Hybrid Model

Production social platforms use a hybrid: push for regular users, pull for celebrities.

Post by regular user (< 10,000 followers):
  → Fan-out on write
  → Async: write post_id to timeline:{follower_id} in Redis for each follower
  → Followers see the post instantly on next feed load

Post by celebrity (> 10,000 followers):
  → Store post in posts table only (no fan-out)
  → Mark this user as "celebrity" in a config set

Feed assembly for User X:
  1. Read pre-built timeline from Redis (from follow-write fan-outs)
  2. Look up all celebrities User X follows
  3. Fetch recent posts from those celebrities directly
  4. Merge the two streams, deduplicate, sort by timestamp
  5. Return top 20 posts
┌──────────────────────────────────────────────────────────┐
│  Feed Assembly Service                                    │
│                                                           │
│  Pre-built timeline                                       │
│  (Redis — regular users' posts)  ──┐                     │
│                                     ├─ merge & sort       │
│  Celebrity posts                    │                     │
│  (DB query at read time)    ────────┘                     │
└──────────────────────────────────────────────────────────┘

The threshold between "regular" and "celebrity" is configurable. Twitter uses a similar hybrid. The exact threshold is tuned based on write/read cost ratios.


Step 4: Data Model

POSTS
  id             UUID  PRIMARY KEY
  user_id        UUID  NOT NULL
  body           TEXT
  media_url      TEXT  (nullable)
  like_count     INT   DEFAULT 0
  repost_count   INT   DEFAULT 0
  created_at     TIMESTAMPTZ

Index: (user_id, created_at DESC) — for celebrity pull queries

FOLLOWS
  follower_id    UUID  NOT NULL
  followee_id    UUID  NOT NULL
  created_at     TIMESTAMPTZ
  PRIMARY KEY (follower_id, followee_id)

Index: (followee_id) — "who follows this user" for fan-out
Index: (follower_id) — "who does this user follow" for feed assembly

TIMELINE (Redis — not a DB table)
  Key:   timeline:{user_id}
  Type:  Sorted Set
  Score: timestamp (for ordering)
  Value: post_id
  Size:  keep last 800 post_ids per user (configurable)

Step 5: Feed Ranking

Pure chronological feeds have a problem: a user who follows 1,000 accounts will miss most posts from accounts they care about because high-volume accounts drown them out.

Basic ranking signals:

score = base_timestamp
      + like_velocity_boost      (posts with fast-growing likes rank higher briefly)
      + affinity_score           (you interact with this account often → higher weight)
      + media_boost              (photos/videos rank above text-only)
      - time_decay               (older posts ranked lower)

For an interview, describe these signals and note that the weights are learned from user engagement data (ML model). The important architectural point: ranking happens at read time, not write time, because ranking signals change after a post is written (likes accumulate).


Step 6: Write Amplification — The Real Cost

Fan-out on write for 580 posts/second with an average 500 followers each = 290,000 Redis writes/second.

For a celebrity with 50M followers posting once: 50M Redis writes. At ~0.5ms per write, serially this takes 7 hours. You need async fan-out workers.

Post created → write to posts DB → enqueue to Kafka topic "new_posts"

Fan-out Worker Pool (scales independently):
  Consumes from Kafka
  For each post: look up follower list in batches of 1,000
  For each batch: pipeline Redis ZADD to update timelines
  Mark complete

Kafka decouples the post write (instant) from the fan-out (background). If fan-out falls behind during a spike, it catches up without affecting posting latency.


Step 7: Storage Sizing

Post storage (PostgreSQL):
  50M posts/day × 365 days = 18.25B posts/year
  Average post: 300 bytes
  18.25B × 300 = ~5.5TB/year — manageable with partitioning by created_at

Timeline cache (Redis):
  500M users × 800 post_ids × 8 bytes = 3.2TB
  → Requires Redis Cluster (shard by user_id)
  → 32 Redis nodes × 100GB RAM each = 3.2TB capacity

Not all 500M users need an active timeline cache entry. An LRU eviction policy means inactive users' timelines are evicted from Redis; on their next login, the feed is rebuilt from the DB (cold start query, ~500ms instead of ~5ms — acceptable for inactive users).


Step 8: Handling Deletions

When a user deletes a post:

  1. Mark post as deleted in the DB (soft delete — deleted_at column)
  2. The timeline caches across all followers still have the post_id

Option A: Proactively remove post_id from all follower timelines (expensive fan-out in reverse)

Option B: Check at read time whether each post_id in the timeline is deleted

Option B is simpler and works: when assembling a feed, fetch posts in batches and filter out any with deleted_at IS NOT NULL. The deleted post disappears from feeds within minutes (next feed load). For most social platforms this is acceptable.


What the Interviewer Is Actually Testing

  • Can you articulate the fan-out on write vs fan-out on read trade-off clearly?
  • Do you propose the hybrid model for celebrity accounts?
  • Do you use Redis sorted sets for the timeline, not a relational DB?
  • Do you understand write amplification and propose async fan-out via Kafka?
  • Do you think about feed ranking as a separate concern from feed assembly?
  • Do you handle deletions without an expensive reverse fan-out?

Related Case Studies

Go Deeper

Case studies teach the "what". Our courses teach the "how" — the patterns behind these decisions, built up from first principles.

Explore Courses