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:
- Post ingestion — accepts new posts, stores them authoritatively, emits events.
- Feed generation — takes raw post events and produces a personalized, ordered list of candidate posts for each user. This is where fan-out happens.
- 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:
| Level | Architecture | Trigger for next level |
|---|---|---|
| 1. Chronological pull | Fetch all followees’ posts on read, sort by time | Read latency grows with follow count |
| 2. Push model | Fan-out on write to per-user inboxes | Celebrity users cause write storms |
| 3. Hybrid push/pull | Push for normal users, pull for celebrities | Chronological feed no longer drives engagement |
| 4. Ranked feed | Add a scoring layer between collection and assembly | Candidate pool exceeds real-time ranking budget |
| 5. Multi-stage ranking | Recall → 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
| Dimension | Push | Pull | Hybrid |
|---|---|---|---|
| Write amplification | O(followers) per post — catastrophic for celebrities | O(1) — one write to author’s outbox | O(followers) only for sub-threshold authors |
| Read amplification | Single-partition scan of one inbox | O(following) — fetch every followee’s outbox | O(celebrity_following) — bounded by the few celebrities a user follows |
| Feed freshness | Bounded by fan-out pipeline lag | Always fresh (reads from authoritative outboxes) | Mixed — pushed posts lag; pulled posts are fresh |
| Operational complexity | Fan-out workers, inbox storage, backpressure | Merge logic at read time, hot outbox partitions | Both — plus a threshold policy to manage |
| Celebrity behavior | Write storms; single post triggers millions of inbox writes | No special cost on write; hot reads on popular outboxes | Celebrities 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
| Decision | Chosen | Rejected | Rationale |
|---|---|---|---|
| Fan-out strategy | Hybrid | Push / Pull | Power-law follower distribution makes pure push catastrophic for celebrities and pure pull too slow for ordinary users |
| Ranking timing | Read-time | Write-time | Ranking signals (engagement, affinity) are dynamic; pre-computed scores go stale in minutes |
| Message queue | Kafka | SQS / RabbitMQ | Replay capability is load-bearing for fan-out correctness; consumer groups fit worker scaling |
| Celebrity threshold | Dynamic | Fixed cutoff | Posting 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:
| Table | Partition key | Role |
|---|---|---|
| inbox | user_id | Pre-materialized candidate posts pushed to each reader |
| outbox | author_id | Per-author post log, pulled at read time for celebrities |
| followers | followee_id | ”Who follows me?” — drives fan-out writes |
| following | follower_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:
| Signal | Why it matters for feeds |
|---|---|
| Recency | Chronological baseline — older posts decay |
| Author-viewer affinity | How often the viewer engages with this author’s content |
| Engagement velocity | How fast the post is accumulating likes/comments globally |
| Content type | Video, image, text — each has different engagement patterns |
| Diversity | Penalty 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:
- Author posts via the API.
- Post service writes to
posts(authoritative) and emits a post event to Kafka. Logical ordering byauthor_idensures per-author event order; physical partitioning strategy is a production detail. - Fan-out workers consume the event. They check the author’s follower count in the follows DB.
- If below threshold (e.g., < 1M followers): fetch the follower list, batch-write
post_idinto each follower’sinbox(both Redis and Cassandra). - 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 /postsreturns 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:
- Feed service fetches recent N post_ids from the user’s pushed inbox (Redis sorted
set, ordered by
created_at). - Feed service looks up the user’s celebrity list from the
followingtable (cached), then fetches recent posts from each celebrity’s outbox. - 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). - Pre-filter: remove already-seen posts and apply content policy. This bounds the ranker’s input.
- Rank: the ranker scores the candidate set dynamically using the signals from Section 6. This is where ordering happens — not in the inbox.
- Assemble: take top K, enforce diversity, attach pagination token.
- Hydrate: fetch full post content from the post cache (Redis hash by post_id, backed by Cassandra on miss).
- 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:
| Requirement | Component | Role |
|---|---|---|
| R1 — Create posts | Post Service | Authoritative write + event emission |
| R2 — Follow users | Follow Graph (two tables) | Maintain follower/following relationships |
| R3 — View feed | Feed Service | Merge inbox + outbox, deduplicate, hydrate |
| R4 — Ranked ordering | Ranker | Score candidates at read time |
| R5 — Engagement | Engagement pipeline | Async counters feeding the ranker |
| Freshness (NFR) | Kafka + fan-out workers | Bounded lag between post and feed appearance |
| Availability (NFR) | Cache layers + degradation paths | Serve 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:
| Query | Frequency | Drives |
|---|---|---|
| Get feed candidates (inbox) | Very high (300K/sec peak) | Inbox partitioned by user_id |
| Get celebrity posts (outbox) | High | Outbox partitioned by author_id |
| Get followers for fan-out | High (3.5M writes/sec feed through this) | Followers partitioned by followee_id |
| Get followees for celebrity pull | High | Following partitioned by follower_id |
| Hydrate post by ID | Very high | Posts partitioned by post_id |
| Get user-author affinity | Medium (ranker reads cached snapshot) | Engagement partitioned by user_id |
Storage technology choices
| Store | Technology | Serves | Why this, not alternatives |
|---|---|---|---|
| Posts | Cassandra | R1 | Write-optimized, partitions by post_id for even distribution, no joins needed |
| Inbox | Cassandra + Redis sorted sets | R3, R4 | Cassandra for durable storage; Redis sorted set for the hot working set |
| Outbox | Cassandra | R3 | Per-author time-sorted log; the access pattern (last N by author) maps directly to clustering keys |
| Following / Followers | Cassandra (two tables) | R2, R3 | Two access patterns on the same data, each needing a single-partition read |
| User profile | Cassandra | R2 | Low-volume, high-read; fan-out workers cache is_celebrity in memory |
| Message queue | Kafka | R1, R3 | Durable, replayable, ordered per author |
Access pattern matrix
All storage access in one view — this is what drives every schema decision:
| Req | Access pattern | Table | Key used | Caller |
|---|---|---|---|---|
| R1 | Write new post | posts | post_id (partition key) | Post service |
| R3 | Hydrate post by ID | posts | post_id (partition key) | Feed service |
| R3 | Get celebrity’s recent posts | outbox | author_id (partition key), created_at DESC (clustering key) | Feed service |
| R3 | Get feed candidates | inbox | user_id (partition key), created_at DESC (clustering key) | Feed service |
| R3 | Write pushed post to inbox | inbox | user_id (partition key) | Fan-out workers |
| R2 | List who I follow | following | follower_id (partition key) | Feed service |
| R2 | List my followers | followers | followee_id (partition key) | Fan-out workers |
| R2 | Check celebrity status | user_profile | user_id (partition key) | Fan-out workers |
| R4 | Get user-author affinity | user_engagement | user_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:
- Load shedding. The ranker drops expensive features under load, using only fast signals (recency, affinity). Feed quality degrades slightly; latency stays within budget.
- Timeout + fallback. If ranking doesn’t respond within 50ms, serve chronological. The user gets a working feed immediately.
- 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_attimestamp. 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 level | They’re probing for | Where to go |
|---|---|---|
| Level 1 | Push vs pull basics | Tradeoff matrix (§5) |
| Level 2 | Celebrity problem | Why hybrid, threshold policy |
| Level 3 | Ranking architecture | Separate service, fallback to chronological |
| Level 4 | Feature pipeline | Feature store, staleness SLOs, streaming jobs |
| Level 5 | Multi-stage ranking | Recall/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
- Twitter’s Timeline at Scale (InfoQ) — the definitive talk on hybrid fan-out, the celebrity problem, and the operational reality of running a large-scale timeline. The clearest primary source on this question.
- Designing Data-Intensive Applications — Kleppmann. Chapters 1, 5, and 11 cover the async-messaging, replication, and stream-processing patterns that this design leans on.
- System Design Interview Vol. 1 — Alex Xu. Chapter 11 is the textbook walkthrough for news feed.
- Twitter Snowflake —
the ID-generation scheme that makes
post_idpartitioning clean. - Power law distribution — the statistical shape that justifies the hybrid strategy.
- Cache-aside pattern — the pattern both cache layers use.
- Martin Fowler: Circuit Breaker — the degradation pattern used across multiple failure modes in this design.