Skip to main content

Walkthrough: Designing a News Feed

A full candidate's-eye walkthrough of the news feed system design question — from the push vs pull fan-out decision through the celebrity problem, ranking, and failure modes.

The problem

You’re asked to design a news feed — Facebook’s home timeline, Twitter’s home timeline, Instagram’s feed. The interviewer says something like: “Design the service that shows a user the posts from people they follow, ordered by time (or relevance). Support posting and viewing the feed.”

Sounds like a list. It isn’t. This is the canonical “fan-out at scale” problem, and the trap is the celebrity problem: if one user is followed by 100M others, a naive design either writes to 100M inboxes on every post (write storm) or scans 100M authors on every read (read storm). Committing to push, pull, or hybrid — and justifying it for this system — is what the question is really testing.

Important

Key takeaway: Feed systems are fundamentally asymmetric between writes and reads. The follower-count distribution is power-law — a tiny fraction of users generate most of the fan-out cost. The entire architecture follows from how you handle that asymmetry.

Below is how I’d walk through this, start to finish.

1. Requirements

First 3–5 minutes. Questions I’d ask to pin down what we’re building:

Functional requirements

  • R1. Users create posts (text, images, video).
  • R2. Users follow other users.
  • R3. Each user sees a feed of posts from the people they follow.
  • R4. Feed is ordered — either chronologically or by relevance.
  • R5. Users can like, comment, and re-share posts.

Scope

This design covers feed retrieval only. Explicitly out of scope:

  • Search / discovery (separate system, different access patterns)
  • Stories / reels (ephemeral content with its own TTL pipeline)
  • Notifications (peer system sharing the fan-out infrastructure)
  • Content moderation / spam (real systems need it; not a design driver here)

Non-functional requirements

  • Scale. How many users? How many posts per user per day? What’s the average follow count? What’s the max follow count (for celebrities)? The system design primer numbers are a reasonable anchor, but for a feed the distribution matters more than the average.
  • Read/write ratio. A timeline is the classic read-heavy system — typically 100:1 or higher. This shapes whether we optimize the read path or the write path.
  • Feed freshness. How stale can a feed be? Seconds? Minutes? A chronological feed has tighter freshness requirements than a ranked one.
  • Availability over consistency. Users can tolerate seeing a slightly stale feed, but a feed that’s down is unacceptable. AP over CP.
  • Ordering. Strictly chronological? Relevance-ranked (ML)? Hybrid? Default for a senior-level interview: relevance-ranked, because that’s what real products do and it forces a richer design.

Say the interviewer confirms: 500M DAU, average user posts 2/day (1B posts/day), average follow count ~300, max ~100M for celebrities, relevance-ranked feed, freshness tolerance ~30 seconds, text + image, support re-shares.

Note

Interview signal: Asking about the distribution of follow counts — not just the average — is what separates a strong requirements phase from a generic one. The follow-count distribution is the single input that determines whether push, pull, or hybrid is the right answer. Interviewers notice when you ask this first.

2. Capacity estimate

  • Posts: 1B/day ≈ 12K writes/sec average, ~50K/sec peak
  • Feed reads: each DAU loads their feed ~10× per day ≈ 5B reads/day ≈ 60K reads/sec average, ~300K/sec peak. Already ~5:1 read/write, and that’s before we count “refreshes” and scroll-driven pagination.
  • Storage: 1B posts/day × 365 days × ~500 bytes (text + metadata, excluding media) ≈ 180 TB/year just for post content. Media goes to object storage separately.
  • Fan-out: if we push, 1B posts × 300 avg followers = 300B writes/day to inboxes. That’s ~3.5M writes/sec. This is the number that decides the architecture.

I’d say out loud: “That 3.5M writes/sec on pure push is the single biggest design constraint. The whole fan-out question is really about whether we can avoid paying that cost for everyone.”

3. High-level architecture

Before committing to any fan-out strategy, I’d sketch the component topology. The feed system has three logical subsystems:

  1. Post ingestion — accepts new posts, stores them authoritatively, emits events.
  2. Feed generation — takes raw post events and produces a personalized, ordered list of candidate posts for each user. This is where fan-out happens.
  3. Feed serving — serves the generated feed to clients via API, handles pagination and hydration.
flowchart TD
  Client[Client] --> API[API Gateway]

  API --> PostSvc[Post Service]
  API --> FeedSvc[Feed Serving]

  PostSvc --> PostsDB[(Posts DB)]
  PostSvc -->|emit event| Queue[(Message Queue)]
  Queue --> FeedGen[Feed Generation]
  FeedGen --> FeedStore[(Feed Store)]

  FeedSvc --> FeedStore
  FeedSvc --> PostSvc

Feed generation is the part that decides the system’s character.

4. Architecture evolution

A feed system in production evolves through recognizable levels. Naming them early helps frame why the design looks the way it does — and what comes next:

LevelArchitectureTrigger for next level
1. Chronological pullFetch all followees’ posts on read, sort by timeRead latency grows with follow count
2. Push modelFan-out on write to per-user inboxesCelebrity users cause write storms
3. Hybrid push/pullPush for normal users, pull for celebritiesChronological feed no longer drives engagement
4. Ranked feedAdd a scoring layer between collection and assemblyCandidate pool exceeds real-time ranking budget
5. Multi-stage rankingRecall → scoring → assembly pipeline(Production system; beyond interview scope)

Each level preserves the previous one as a fallback — Level 4 degrades to Level 3 when the ranker is down; Level 3 degrades to Level 2 for users who follow no celebrities.

The design in this walkthrough is Level 4: hybrid fan-out with a ranking layer. In an interview, landing at Level 3–4 with awareness of Level 5 is a strong Staff+ signal.

I’d say out loud: “I’m designing for Level 4 — hybrid with ranking. But the system should degrade gracefully to Level 3 under failure, and the storage architecture shouldn’t need to change if we later evolve to Level 5.”

Note

Interview signal: Naming the architecture evolution levels — and placing your design within them — demonstrates that you understand the system as a living thing, not a static blueprint. It also gives you a framework for answering “what would you do next?” follow-ups without improvising.

Target architecture (Level 4)

This is the complete system we’re designing. Every section that follows explains one part of this diagram:

flowchart TD
  Client[Client] --> API[API Gateway]

  API --> PostSvc[Post Service]
  API --> FeedSvc[Feed Service]

  PostSvc --> PostsDB[(Posts)]
  PostSvc -->|event| Kafka[(Kafka)]
  Kafka --> Fanout[Fan-out Workers]
  Fanout --> FollowsDB[(Followers)]
  Fanout --> InboxDB[(Inbox)]

  FeedSvc --> InboxCache[(Inbox Cache)]
  InboxCache -->|miss| InboxDB
  FeedSvc --> Outbox[(Outbox)]
  FeedSvc --> Ranker[Ranker]
  FeedSvc --> PostCache[(Post Cache)]
  PostCache -->|miss| PostsDB

  classDef store fill:#eef2f1,stroke:#2c5f5d,stroke-width:2px,color:#1f2937;
  class PostsDB,Kafka,FollowsDB,InboxDB,InboxCache,Outbox,PostCache store

5. Core design choice: fan-out strategy

Important

Key takeaway: The central question in a feed system is where fan-out occurs — on the write path (push), on the read path (pull), or selectively on both (hybrid). Most architectural decisions follow from this choice.

This is the question. Three options:

(a) Fan-out on write (push). When a user posts, write the post_id into the inbox of every follower. Each user’s feed is then a trivial read: scan your own inbox, hydrate post content, return.

(b) Fan-out on read (pull). Keep posts in each author’s outbox. On feed read, fetch each followee’s recent outbox, merge, rank, return.

(c) Hybrid. Push for ordinary users; pull for celebrities. A user’s feed is (pushed inbox) ∪ (pulled celebrity outboxes) — merged and ranked at read time.

Why hybrid wins

The justification is topic-specific: the follower-count distribution in social networks is power-law — a tiny fraction of users account for most of the fan-out cost. Push the 99.9%, pull the 0.1%. This is exactly the architecture Twitter described for their home timeline, and the reasoning generalizes: in any system where the cost distribution is power-law, the fix is a per-entity policy, not a one-size-fits-all protocol.

The threshold is tunable (say, 1M followers). Users above threshold don’t fan out on write; their posts are pulled at read time and merged into followers’ feeds. Below threshold, standard push.

Warning

Production reality: Real systems rarely use a single fixed celebrity threshold. The threshold is often dynamic, factoring in: current follower count, author’s posting frequency (a celebrity who posts 50× per day costs more to fan out than one who posts once), current system load (the threshold can tighten during traffic spikes to shed load), and the marginal fan-out cost per post. Some systems compute a per-author “fan-out budget” rather than a simple follower-count cutoff.

Threshold transitions are forward-looking. When an author crosses from 999K to 1.1M followers, existing inbox entries (already pushed) are left untouched — they’ll age out naturally via TTL or be overwritten by newer entries. Only future posts switch from push to pull. This is why the read path deduplicates: during the transition window, some posts exist in both the inbox (pushed before threshold) and the outbox (pulled after threshold).

Tradeoff matrix

DimensionPushPullHybrid
Write amplificationO(followers) per post — catastrophic for celebritiesO(1) — one write to author’s outboxO(followers) only for sub-threshold authors
Read amplificationSingle-partition scan of one inboxO(following) — fetch every followee’s outboxO(celebrity_following) — bounded by the few celebrities a user follows
Feed freshnessBounded by fan-out pipeline lagAlways fresh (reads from authoritative outboxes)Mixed — pushed posts lag; pulled posts are fresh
Operational complexityFan-out workers, inbox storage, backpressureMerge logic at read time, hot outbox partitionsBoth — plus a threshold policy to manage
Celebrity behaviorWrite storms; single post triggers millions of inbox writesNo special cost on write; hot reads on popular outboxesCelebrities are exempt from push; cost shifts to a bounded read-time pull

The hybrid’s cost: operational complexity. You run both pipelines and maintain a threshold policy. The payoff: you avoid the unbounded cost of either extreme. For a social network with power-law follow distributions, this tradeoff is definitively worth it — the alternative is paying 3.5M writes/sec on average with spikes orders of magnitude higher.

Why inbox and outbox coexist

This is worth saying explicitly, because interviewers ask: “Why not just one or the other?”

  • The inbox exists because reads are frequent and must be fast. Pre-materializing a user’s feed into their inbox makes the read path a single-partition scan.
  • The outbox exists because some authors are too expensive to push. The outbox is the author’s canonical post log. It’s always correct, always fresh, and costs nothing to maintain — conceptually it’s the author’s post log, physically implemented as a separate table optimized for author-based time-ordered access.
  • They serve different populations. The inbox serves the 99.9% of posts from ordinary authors. The outbox serves the 0.1% from celebrities. Neither alone handles both.

Inbox and outbox are both materialized views of the same social graph, optimized for different access directions. The inbox materializes “what should I read?” (optimized for the reader). The outbox materializes “what did I write?” (optimized for the author’s followers to pull from).

Caution

Common mistake: Using a single follows table for both “who do I follow?” and “who follows me?” queries. These are inverse access patterns that require different partition keys. A single table forces either a secondary index (scatter-gather query under load) or a full table scan. Two denormalized tables — following and followers — each with the correct partition key, is the only design that gives single-partition reads for both fan-out workers and the feed service.

Architecture decisions

DecisionChosenRejectedRationale
Fan-out strategyHybridPush / PullPower-law follower distribution makes pure push catastrophic for celebrities and pure pull too slow for ordinary users
Ranking timingRead-timeWrite-timeRanking signals (engagement, affinity) are dynamic; pre-computed scores go stale in minutes
Message queueKafkaSQS / RabbitMQReplay capability is load-bearing for fan-out correctness; consumer groups fit worker scaling
Celebrity thresholdDynamicFixed cutoffPosting frequency and system load matter as much as follower count

Core data structures

Before diving into the read and write paths, the four tables that make the hybrid fan-out design possible:

TablePartition keyRole
inboxuser_idPre-materialized candidate posts pushed to each reader
outboxauthor_idPer-author post log, pulled at read time for celebrities
followersfollowee_id”Who follows me?” — drives fan-out writes
followingfollower_id”Who do I follow?” — drives celebrity pull at read time

Full schemas and query patterns are in §10. What matters now: each table is partitioned so its primary access pattern is a single-partition read — no secondary indexes on the hot paths.

6. Feed generation pipeline [R3, R4]

Important

Key takeaway: Candidate collection determines completeness. Ranking determines ordering. Keep them separate — this separation is what allows the ranker to fail independently without breaking the feed.

Feed generation is the bridge between raw posts and user-visible feeds. It has three stages:

Candidate collection

Assemble the pool of posts that could appear in a user’s feed:

  • Pushed posts — already in the user’s inbox from fan-out on write.
  • Pulled posts — fetched from celebrity outboxes at read time.
  • Injected posts (optional) — ads, recommended posts from non-followed accounts.

The candidate set is typically 500–2000 posts. The job of collection is completeness — don’t rank what you haven’t collected.

Ranking

Score each candidate post. Signals, in rough order of importance for a social feed:

SignalWhy it matters for feeds
RecencyChronological baseline — older posts decay
Author-viewer affinityHow often the viewer engages with this author’s content
Engagement velocityHow fast the post is accumulating likes/comments globally
Content typeVideo, image, text — each has different engagement patterns
DiversityPenalty for showing 5 posts from the same author consecutively

The ranker produces a score per candidate. For an interview, I’d name the signals and say the model is a learned function trained on engagement outcomes. The ML pipeline itself is out of scope — implementing ranking models is a separate system design question.

For interview-scale systems (Level 4), candidate collection acts as the recall stage. Large production systems (Level 5) separate recall into multiple retrieval pipelines (e.g., interest-graph recall, trending-content recall, collaborative-filtering recall) before a unified scoring pass. The boundary is the same — just more sources feeding into it.

Warning

Production reality: Ranking features like affinity and engagement velocity are usually eventually consistent. Updates flow through a pipeline: engagement events → Kafka → streaming aggregation jobs (Flink/Spark Streaming) → feature store (Redis or Feast). The ranker reads from the feature store — slightly stale but continuously refreshed. Typical staleness: seconds for engagement velocity, minutes for affinity. This is acceptable because ranking is a “best effort” optimization — slightly stale features produce slightly suboptimal ordering, not incorrect feeds. The feature store described here is the online serving store (low-latency reads during feed generation). Offline training pipelines read from a separate batch store — same features, different SLOs.

Assembly

Take the top K scored candidates, enforce diversity constraints (no more than 2 consecutive posts from one author), attach a continuation token for pagination.

A lightweight pre-filter may reduce the candidate set before ranking (e.g., remove posts the user has already seen, apply content policy filters). This keeps the ranker’s input bounded even as the candidate pool grows.

Fallback behavior: if the ranking service is unavailable, fall back to chronological ordering over the candidate set. The feed is still usable — just less personalized. This is why separating ranking from collection matters architecturally: you can remove the ranker and the system still works.

7. Write path [R1, R2 → R3]

Extending the high-level architecture with the write-path components:

flowchart TD
  Author[Author] --> API[API Gateway]
  API --> PostSvc[Post Service]
  PostSvc --> PostsDB[(Posts · Cassandra)]
  PostSvc -->|post event| Kafka[(Kafka)]

  Kafka --> Fanout[Fan-out Workers]
  Fanout --> Follows[(Follows DB)]
  Fanout --> InboxDB[(Inbox · Cassandra)]
  Fanout --> InboxCache[(Inbox · Redis)]

  classDef store fill:#eef2f1,stroke:#2c5f5d,stroke-width:2px,color:#1f2937;
  class PostsDB,Kafka,Follows,InboxDB,InboxCache store

Step by step:

  1. Author posts via the API.
  2. Post service writes to posts (authoritative) and emits a post event to Kafka. Logical ordering by author_id ensures per-author event order; physical partitioning strategy is a production detail.
  3. Fan-out workers consume the event. They check the author’s follower count in the follows DB.
  4. If below threshold (e.g., < 1M followers): fetch the follower list, batch-write post_id into each follower’s inbox (both Redis and Cassandra).
  5. If above threshold: skip fan-out entirely. The post stays in posts; it will be pulled at read time by any follower viewing their feed.

Tip

Why async via Kafka: Three reasons, each tied to the fan-out problem specifically:

  • Decouples write latency from fan-out work. The author’s POST /posts returns as soon as the authoritative write lands — not after millions of inbox writes. Without this decoupling, a user with 500K followers would wait seconds for their post to “publish.”
  • Absorbs celebrity-adjacent bursts. Users near the threshold (say, 800K followers) still fan out. When multiple near-threshold users post simultaneously, the queue smooths the load spike.
  • Enables replay. If a fan-out worker bug drops events, we replay from Kafka. If we add a new downstream consumer (notifications, analytics), we replay to bootstrap it.

8. Read path [R3, R4]

Important

Key takeaway: The read path merges two data sources (pushed inbox + pulled celebrity outboxes), deduplicates, ranks, and hydrates — in that order. Each step can fail independently, and the system remains usable by skipping the failed step.

flowchart TD
  Client[Client] --> API[API Gateway]
  API --> FeedSvc[Feed Service]

  FeedSvc --> InboxCache[(Inbox · Redis)]
  InboxCache -->|miss| InboxDB[(Inbox · Cassandra)]
  FeedSvc --> CelebOutbox[(Celebrity Outbox)]

  FeedSvc --> Ranker[Ranker]
  Ranker --> FeedSvc

  FeedSvc --> PostCache[(Posts · Redis)]
  PostCache -->|miss| PostsDB[(Posts · Cassandra)]

  classDef store fill:#eef2f1,stroke:#2c5f5d,stroke-width:2px,color:#1f2937;
  class InboxCache,InboxDB,CelebOutbox,PostCache,PostsDB store

Feed read, step by step:

  1. Feed service fetches recent N post_ids from the user’s pushed inbox (Redis sorted set, ordered by created_at).
  2. Feed service looks up the user’s celebrity list from the following table (cached), then fetches recent posts from each celebrity’s outbox.
  3. Merge and deduplicate: combine pushed and pulled candidates. The merge step requires a post_id-based deduplication pass before ranking (threshold transitions can cause the same post to appear in both sources).
  4. Pre-filter: remove already-seen posts and apply content policy. This bounds the ranker’s input.
  5. Rank: the ranker scores the candidate set dynamically using the signals from Section 6. This is where ordering happens — not in the inbox.
  6. Assemble: take top K, enforce diversity, attach pagination token.
  7. Hydrate: fetch full post content from the post cache (Redis hash by post_id, backed by Cassandra on miss).
  8. Return to client.

Caution

Common mistake: Skipping deduplication after the merge step. If an author crosses the celebrity threshold while some followers still have their recent posts in the inbox (from prior fan-out), those posts will appear in both the inbox and the pulled outbox. Without dedup on post_id, users see duplicate posts — a visible, user-facing bug.

Why two cache tiers

  • Inbox cache (Redis sorted set per active user, scored by created_at). Holds recent N post_ids as candidates for ranking. TTL-based: warm on read, evict when cold.
  • Post content cache (Redis hash by post_id). Holds hydrated post bodies. Posts are effectively immutable (edits rare, deletes rarer), so this cache needs almost no invalidation — a long TTL is fine.

They have different invalidation properties. The inbox cache must be write-through (new pushes must appear immediately); the post cache is essentially read-only. Separate caches let each use the invalidation strategy that matches its data’s mutation frequency.

9. API design [R1, R2, R3, R4]

Three endpoints do most of the work:

POST /api/posts                              [R1 — create posts]
  body:    { author_id, text, media_ids?, parent_post_id? }
  returns: { post_id, created_at }

GET /api/feed?next_token=<opaque>&limit=20   [R3, R4 — view ranked feed]
  returns: { posts: [...], next_token }

POST /api/follow                             [R2 — follow users]
  body:    { follower_id, followee_id }
  returns: { ok }

Non-obvious decision: token-based pagination, not offset-based. Offset-based pagination breaks when new posts arrive between pages — the user sees duplicates or misses posts. A next_token is an opaque value encoding enough state to resume (e.g., the ranked_at timestamp of the last-seen page plus a tie-breaker post_id). It’s stable against inserts. Worth saying out loud; interviewers notice.

Caution

Common mistake: Using offset-based pagination (?page=2&limit=20) for a feed. Between page 1 and page 2 requests, new posts arrive and shift the entire list. Token-based pagination anchors to a stable point in the ordered set, making it immune to concurrent inserts.

The GET /api/feed endpoint abstracts over the entire feed generation pipeline — the client doesn’t know or care whether a post was pushed or pulled, or what ranking model scored it. This abstraction boundary means the backend can evolve from Level 3 to Level 5 without API changes.

10. Data model and storage

Requirements → components

Before diving into storage, how each requirement maps to the components that serve it:

RequirementComponentRole
R1 — Create postsPost ServiceAuthoritative write + event emission
R2 — Follow usersFollow Graph (two tables)Maintain follower/following relationships
R3 — View feedFeed ServiceMerge inbox + outbox, deduplicate, hydrate
R4 — Ranked orderingRankerScore candidates at read time
R5 — EngagementEngagement pipelineAsync counters feeding the ranker
Freshness (NFR)Kafka + fan-out workersBounded lag between post and feed appearance
Availability (NFR)Cache layers + degradation pathsServe a feed even when subsystems fail

Access frequency

The relative query frequency drives every schema decision — high-frequency queries must be single-partition reads with no secondary indexes:

QueryFrequencyDrives
Get feed candidates (inbox)Very high (300K/sec peak)Inbox partitioned by user_id
Get celebrity posts (outbox)HighOutbox partitioned by author_id
Get followers for fan-outHigh (3.5M writes/sec feed through this)Followers partitioned by followee_id
Get followees for celebrity pullHighFollowing partitioned by follower_id
Hydrate post by IDVery highPosts partitioned by post_id
Get user-author affinityMedium (ranker reads cached snapshot)Engagement partitioned by user_id

Storage technology choices

StoreTechnologyServesWhy this, not alternatives
PostsCassandraR1Write-optimized, partitions by post_id for even distribution, no joins needed
InboxCassandra + Redis sorted setsR3, R4Cassandra for durable storage; Redis sorted set for the hot working set
OutboxCassandraR3Per-author time-sorted log; the access pattern (last N by author) maps directly to clustering keys
Following / FollowersCassandra (two tables)R2, R3Two access patterns on the same data, each needing a single-partition read
User profileCassandraR2Low-volume, high-read; fan-out workers cache is_celebrity in memory
Message queueKafkaR1, R3Durable, replayable, ordered per author

Access pattern matrix

All storage access in one view — this is what drives every schema decision:

ReqAccess patternTableKey usedCaller
R1Write new postpostspost_id (partition key)Post service
R3Hydrate post by IDpostspost_id (partition key)Feed service
R3Get celebrity’s recent postsoutboxauthor_id (partition key), created_at DESC (clustering key)Feed service
R3Get feed candidatesinboxuser_id (partition key), created_at DESC (clustering key)Feed service
R3Write pushed post to inboxinboxuser_id (partition key)Fan-out workers
R2List who I followfollowingfollower_id (partition key)Feed service
R2List my followersfollowersfollowee_id (partition key)Fan-out workers
R2Check celebrity statususer_profileuser_id (partition key)Fan-out workers
R4Get user-author affinityuser_engagementuser_id (partition key), author_id (clustering key)Ranker

Schemas

Posts [R1]

posts (Cassandra):
  partition key:  post_id
  columns:        author_id, text, media_ids[], parent_post_id,
                  created_at, engagement_counts { likes, comments, shares }

post_id as partition key gives even distribution (Snowflake IDs). All author-timeline access goes through the outbox table below — no secondary index needed on posts.

Outbox [R3]

outbox (Cassandra):
  partition key:   author_id
  clustering key:  created_at DESC
  columns:         post_id

Serves both celebrity pull at read time and author profile pages. This is the per-author chronological log that makes the secondary index on posts.author_id unnecessary.

Warning

Production reality: High-volume authors (celebrities posting frequently over years) create unbounded outbox partitions. Same solution as inbox: bucket by time window — e.g., partition key (author_id, month). The feed service only reads the most recent bucket(s) since it needs at most the last N posts.

Inbox [R3, R4]

inbox (Cassandra):
  partition key:   user_id
  clustering key:  created_at DESC
  columns:         post_id, author_id

Clustering key is created_at, not a ranking score. Ranking is computed dynamically at read time. The inbox is a candidate buffer — it stores what’s available to rank, not the ranking itself.

Caution

Common mistake: Storing ranking scores inside the Cassandra inbox rows. This seems efficient (“pre-rank at write time!”) but ranking signals change constantly — engagement velocity shifts minute-to-minute, affinity updates as the user interacts with new content. Updating Cassandra clustering keys requires a delete + re-insert, which is expensive at fan-out scale.

Warning

Production reality: A single user_id partition grows unbounded over time (years of pushed posts). Production systems bucket the inbox by time window — e.g., partition key (user_id, month) — to cap partition size. The feed service reads only the current and previous bucket. Staff-level interviewers frequently probe this.

Following [R2, R3]

following (Cassandra):
  partition key:   follower_id
  clustering key:  followee_id
  columns:         created_at

Followers [R2, R3]

followers (Cassandra):
  partition key:   followee_id
  clustering key:  follower_id
  columns:         created_at

Two tables, not one. Fan-out is the hottest path (3.5M writes/sec). It cannot depend on a secondary index — those are scatter-gather queries in Cassandra. Fan-out workers need SELECT follower_id FROM followers WHERE followee_id = ? — a single-partition read. The feed service needs the inverse: SELECT followee_id FROM following WHERE follower_id = ?. Two tables, each optimized for its caller.

User profile [R2]

user_profile (Cassandra):
  partition key:  user_id
  columns:        display_name, followers_count,
                  is_celebrity (boolean — followers_count > threshold),
                  account_state (active | suspended | deactivated)

followers_count is updated asynchronously (increment on follow, decrement on unfollow). is_celebrity flips when it crosses the threshold — cheap to read, cached in memory by fan-out workers.

User engagement [R4, R5]

user_engagement (Cassandra):
  partition key:   user_id
  clustering key:  author_id
  columns:         interaction_count, last_interaction_at

Feeds the affinity signal to the ranker. Updated asynchronously from engagement events. Not on the critical read path — the ranker reads a cached snapshot.

11. Deep dives

Feed freshness and cache invalidation

The tension: caching makes reads cheap, but stale caches make feeds feel dead.

For the inbox cache, freshness comes from the write path — a new pushed post writes through to Redis immediately. The cache is always as fresh as the fan-out pipeline. Staleness here means fan-out lag (Kafka consumer lag), not cache expiry.

For pulled celebrity posts, freshness depends on when the feed service fetches the outbox. We accept ~30 seconds of staleness here — the user’s feed refresh triggers a pull, and the outbox is always authoritative.

For the post content cache, invalidation is a non-problem: posts rarely change. Edits (if supported) write through; deletes set a tombstone that propagates via an invalidation event on the Kafka topic.

The key insight: “In a feed system, freshness is primarily a write-path concern (how fast does fan-out complete?), not a cache-invalidation concern. The caches are downstream of the pipeline, not independent of it.”

Hot keys and celebrity read amplification

When a celebrity posts and millions of followers refresh their feeds within seconds, the celebrity’s outbox partition becomes a hot read key. This is the read-side celebrity problem — distinct from the write-side problem that hybrid solves. Hybrid eliminates write storms, but creates a concentrated read pattern on outbox partitions.

Why this happens: At 300K feed reads/sec, if even 1% of those users follow a given celebrity, that’s 3K reads/sec hitting one outbox partition. The hot key isn’t a bug — it’s a direct, predictable consequence of the hybrid design choice. The number of celebrities is small and known, which means the mitigations are bounded.

Note

Interview signal: Identifying the read-side celebrity problem as a consequence of the hybrid design — not a separate, unrelated problem — is a Staff+ signal. The interviewer may ask: “What’s the downside of hybrid?” This is the answer.

From chronological to ranked

A chronological feed is a sorted merge. A ranked feed introduces a scoring function between candidate collection and assembly. This is when the system crosses from Level 3 to Level 4, and the architectural implications are significant:

  • New dependency: the ranker service. Must be low-latency (p99 < 50ms for ~1000 candidates) and high-availability.
  • New failure mode: ranker unavailability. The system must degrade to chronological without operator intervention. This is why ranking is a separate service, not embedded in the feed service — so it can fail independently.
  • New tuning surface: the ranking model is the product. Changes to it change what users see. This creates a feedback loop — the model optimizes for engagement, which changes user behavior, which changes the training data.

The transition from chronological to ranked is when the feed system stops being purely an infrastructure problem and becomes a product-ML-infrastructure hybrid. For an interview, naming this transition and its implications is more valuable than describing the ML model.

12. Failure modes

Important

Key takeaway: Every failure in a feed system threatens one of three properties: freshness (are posts appearing promptly?), latency (are reads fast?), or correctness (is the content right?). Knowing which property a failure threatens tells you what degradation is acceptable.

Six scenarios, grouped by what they threaten:

Freshness failures

Fan-out workers fall behind (Kafka consumer lag)

Threatens: freshness.

Kafka absorbs the backlog. Followers see delayed posts but no data loss. The feed is stale, not wrong. Mitigation: scale workers horizontally; Kafka consumer groups handle rebalancing. Monitor consumer lag as the primary health metric — it’s the single best proxy for feed freshness. If lag exceeds the freshness SLO (30 seconds), page someone.

Why this is the most likely failure: fan-out is the highest-throughput component (3.5M writes/sec on average). Any upstream traffic spike (viral event, celebrity posting spree) hits the fan-out workers first.

Latency failures

Ranking service overload

Threatens: latency (and feed quality under degradation).

During traffic spikes, the ranker’s candidate volume spikes. Mitigations, in degradation order:

  1. Load shedding. The ranker drops expensive features under load, using only fast signals (recency, affinity). Feed quality degrades slightly; latency stays within budget.
  2. Timeout + fallback. If ranking doesn’t respond within 50ms, serve chronological. The user gets a working feed immediately.
  3. Pre-ranked cache. For frequently-accessed feeds (returning visitors within 60 seconds), serve the previously-ranked result. Stale-ranked beats unranked.

The design principle: ranking is an enhancement, not a prerequisite.

Redis inbox cluster loses a node

Threatens: latency.

Thundering herd of cold reads onto Cassandra. Mitigations: request coalescing (one miss per user triggers one DB read, not N), circuit breakers — see Martin Fowler on the pattern — and pre-warming from a secondary replica set. The feed degrades to slower reads, not broken reads.

Celebrity traffic spike on the read path

Threatens: latency.

A celebrity posts, millions of followers refresh simultaneously — the hot-key problem described in §11. Mitigations, in order of impact:

  • Outbox replication in Redis. The celebrity’s recent posts are replicated across multiple Redis shards. Feed service round-robins across replicas.
  • Read coalescing. Multiple simultaneous requests for the same outbox resolve to a single backend read, with results fanned out to all waiters.
  • Pre-computed snapshots. For the top ~1000 accounts by follower count, pre-generate their outbox snapshots every few seconds. Serve from edge caches.

Correctness failures

Feed cache corruption or stale ranking

Threatens: correctness.

If a deleted post appears in someone’s feed, or a rolled-back ranking model serves stale scores:

  • Soft deletes propagate. A delete event invalidates the post in both the post cache and any inbox entry referencing it.
  • Version stamps. Cached ranking results carry a ranked_at timestamp. The feed service discards results older than a freshness threshold on the next read.
  • Model version tagging. A ranking model rollback triggers background re-ranking for active users. Inactive users get re-ranked on their next feed load.

Follows DB partition unavailable

Threatens: correctness (and freshness for affected users).

Fan-out workers for affected partitions stall. This is the hardest failure because it’s correctness-adjacent — if we can’t fetch the follower list, we can’t fan out correctly when the partition recovers. Mitigation: follows should be in a quorum-replicated store. On recovery, replay queued events from Kafka to catch up the missed fan-outs.

Note

Interview signal: Distinguishing between failures that affect freshness, latency, and correctness — and explaining what degradation is acceptable for each — is what separates Senior from Staff answers on this question.

13. What I’d skip, and say I’m skipping

Time check — five minutes left. Things I’d explicitly defer:

  • The ranking model itself. I’ve named the signals, the pipeline shape, and the fallback behavior. Implementing the ML (feature stores, model serving, training loops) is a separate system.
  • Media pipeline. Images and video are stored in object storage behind a CDN, with transcoding for different resolutions. Say “CDN + S3” and move on.
  • Notifications. A peer system to the feed, sharing the fan-out infrastructure. Worth a sentence; not a section.
  • Spam and integrity. Content moderation, rate limits on posting, fake accounts. Real systems need this; interviews don’t require you to solve it.
  • Multi-stage candidate generation (Level 5). When the candidate pool exceeds what a single ranker pass can handle, you add a recall/scoring/assembly funnel. I’ve named it in the evolution table; building it out is a separate design.

Saying “I’d skip this, and here’s why” is a strong signal. It shows you know the full surface and are making deliberate scoping choices.

Interview flow summary

Architecture at a glance

WRITE PATH                          READ PATH

Post                                Client request
 ↓                                   ↓
Post Service                        Feed Service
 ↓                                   ↓
Kafka                               Inbox (pushed) + Outbox (pulled)
 ↓                                   ↓
Fan-out Workers                     Merge + Dedup
 ↓                                   ↓
Inbox (per follower)                Pre-filter → Rank → Assemble

                                    Hydrate → Response

The walkthrough order for your whiteboard

1. Requirements & scope    — pin down scale, distribution, freshness
2. Capacity estimate       — find the 3.5M writes/sec constraint
3. Architecture evolution  — name Levels 1–5; target Level 4
4. Fan-out strategy        — hybrid; justify from power-law
5. Feed pipeline           — collect → pre-filter → rank → assemble
6. Write path              — post → Kafka → fan-out (skip celebrities)
7. Read path               — inbox ∪ outbox → dedup → rank → hydrate
8. API                     — three endpoints, token pagination
9. Data model              — access pattern matrix → schemas
10. Deep dives             — freshness, hot keys, ranked transition
11. Failure modes          — freshness / latency / correctness

If the interviewer pushes deeper at any point:

Depth levelThey’re probing forWhere to go
Level 1Push vs pull basicsTradeoff matrix (§5)
Level 2Celebrity problemWhy hybrid, threshold policy
Level 3Ranking architectureSeparate service, fallback to chronological
Level 4Feature pipelineFeature store, staleness SLOs, streaming jobs
Level 5Multi-stage rankingRecall/scoring/assembly funnel (name it, don’t build it)

14. Wrap-up

One crisp sentence before the interviewer’s next question:

This design optimizes feed reads by precomputing most users’ inboxes via push, handles the celebrity fan-out problem by pulling at read time for the 0.1% where push doesn’t scale, and layers a ranking pipeline between candidate collection and feed assembly so the system can evolve from chronological to relevance-ranked without changing the underlying storage architecture.

What separates levels on this question

  • SDE II usually lands on push or pull, describes one fan-out strategy, names the inbox/outbox structure, and sketches a basic read path.
  • SDE III names the power-law follower distribution as the reason for hybrid, picks a threshold with a specific rationale, treats the async fan-out pipeline as a peer system (not an implementation detail), names 4+ failure modes with graceful degradation paths, and explicitly defers ranking and media as “second systems.”
  • Staff/Principal explains why inbox and outbox coexist (not just that they do); names the architecture evolution levels and where this design sits; separates ranking as an independent service with its own failure/degradation contract; identifies the read-side celebrity problem as a consequence of the hybrid design (not just a separate problem); and frames tradeoffs in terms of cost asymmetries, not feature checklists.

The difference isn’t knowledge. It’s naming the asymmetry — that the cost distribution is non-uniform, the design should be non-uniform to match, and the system evolves through identifiable architectural stages as product requirements grow.

Further reading