3. Twitter / News Feed Intermediate → Advanced

A. Realistic Interview Prompt

"Design a system like Twitter: users post short text/media updates, follow other users, and see a reverse-chronological (or ranked) feed of posts from the people they follow."

This is the canonical "fanout" problem — the entire design hinges on one question: do you compute a user's feed when they post, or when their followers read? Both answers are individually simple; the difficulty is that neither answer works uniformly for every user, because follower-count distributions on a real social graph are wildly skewed (a handful of accounts with 100M+ followers, the vast majority with a few hundred). A design that treats all users identically has already missed the core insight.

B. Difficulty

Intermediate to start — "write posts to a table, read posts from people you follow" is a five-minute answer — escalating to Advanced the moment the interviewer asks what happens when a celebrity with 100M followers posts, or when the feed needs to be ranked instead of chronological. Structurally similar to Rate Limiter in that a clean naive answer exists, but the real signal is entirely in the deep dives.

C. What This Problem Tests

D. Requirements

Functional

  1. Users can post short text updates (optionally with media) and follow/unfollow other users
  2. Users can view a feed composed of posts from people they follow, ordered by recency (baseline) or relevance (extension, Q)
  3. Users can like, reply to, and repost/retweet content

Non-Functional

Out of scope: direct messages, notifications infrastructure, ad insertion, detailed content moderation — each is its own design problem.

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Chronological feed, or ranked/algorithmic?Chronological is a pure delivery problem; ranked adds a scoring/ML layer on topDeep Dive 5 (Q)
2What's the follower-count distribution — are there celebrity accounts with tens of millions of followers?This single fact is what forces a hybrid fanout design instead of one uniform strategyDeep Dive 1-2 (M, N)
3How stale can a feed be before it's a problem?Confirms eventual consistency is acceptable, unlocking async fanout entirelyConsistency Model (V)
4Is search (by keyword/hashtag) in scope?A materially different data-access pattern (full-text) from timeline deliveryDeep Dive 7 (S)
5How far back does "the feed" need to go — infinite scroll, or a bounded recent window?Bounds the size of the precomputed inbox structure in OTimeline Store (O)
6Do posts support media (images/video)?Changes storage and delivery entirely — blob store + CDN, not the feed DBDeep Dive 6 (R)

F. Assumptions

G. Capacity Estimation

MetricValue / CalculationWhy It Matters
DAU200MBase input for every number below
Posts/day200M × 10% ≈ 20M/dayWrite volume — small relative to reads
Post write QPS20M ÷ 86,400s ≈ ~230/sec (peak ~3-5×, ~1K/sec)Trivial write load in isolation — the real cost is fanout, not the write itself
Feed reads/day200M × 5 ≈ 1B/dayRead volume dwarfs write volume by ~50×
Feed read QPS1B ÷ 86,400s ≈ ~11,600/sec (peak ~35-50K/sec)Confirms this is a read-optimize-first problem — justifies precomputing feeds instead of computing on read
Average follower count~200 (typical), but a small set of accounts have 10M-100M+The skew itself, not the average, is what drives the celebrity-problem deep dive (N)
Fanout writes for one average post~200 (one per follower)Cheap — fine to do synchronously-ish for typical users
Fanout writes for one celebrity post (10M followers)10,000,000 individual inbox writesAt Twitter/X's own reported scale this can take over an hour to fully fan out if done the same way as a normal user — the concrete number that forces the hybrid design in N
Post storage/year20M/day × 365 × ~300B/post (text+metadata) ≈ ~2TB/year (media stored separately in blob store, R)Trivial for the source-of-truth post store; confirms media, not text, is the real storage driver

H. Core Entities

EntityPurposeKey Fields
UserAn accountuser_id, username, follower_count (denormalized — read constantly, see N)
PostSource of truth for one postpost_id, author_id, text, media_url, created_at
FollowEdge in the social graphfollower_id, followee_id, created_at
FeedEntry (precomputed)One row per (follower, post) — the denormalized "inbox"user_id (the feed owner), post_id, author_id, created_at — deliberately duplicated data, not a foreign-key join (O)

I. API Design

POST /v1/posts               { text, media_url? } -> { post_id, created_at }
POST /v1/follows             { followee_id }
DELETE /v1/follows/{id}
GET  /v1/feed?cursor=&limit= -> { posts: [...], next_cursor }
POST /v1/posts/{id}/like

Why a cursor, not an offset, for pagination: the feed is a live, constantly-growing precomputed list (O) — an offset-based page 2 can skip or duplicate entries as new posts fan in between requests; a cursor (typically an encoded timestamp+post_id) is stable against concurrent inserts, the same reasoning behind cursor-based pagination in most high-write-throughput feeds.

J. Basic / Naive High-Level Design (V1) — Fanout-on-Read

The simplest correct design: store posts in one table; to build a feed, query the posts of everyone the user follows, at read time, and merge-sort by recency.

flowchart LR U(["User opens feed"]) --> API["Feed Service"] API -- "1. get following list" --> Graph[("Follow Graph DB")] API -- "2. query posts for each followee" --> Posts[("Post DB")] API -- "3. merge + sort in memory" --> API API --> U

V1 — nothing is precomputed. Every feed load re-queries and re-merges from scratch.

Why this doesn't scale, specifically: if a user follows 500 accounts, loading their feed means 500 separate lookups (or one large fanned-out query) plus an in-memory merge, on every single feed load — and per G, feed loads happen ~50× more often than posts are written. This design does the most expensive part of the work (the fanout/merge) at the highest-frequency point in the whole system (read time) — precisely backwards for a read-heavy workload. It is, however, immediately correct for new-post visibility (no fanout lag at all) and worth naming as V1's one genuine advantage before replacing it.

K. Data Model & Store Choice

Posts and the follow graph: a relational or wide-column store (e.g. Postgres/MySQL for posts, or a graph-friendly store for follows) — this data is the durable source of truth and benefits from real schema/indexing.

The precomputed feed/inbox (FeedEntry): a wide-column store or Redis sorted set, keyed by user_id, with post_ids ordered by a score (timestamp, or a rank score once Q is layered in). Why a sorted set, not a relational table: the access pattern is "give me the top N entries for this user, ordered by score" — exactly what a sorted set's ZREVRANGE does in O(log n), and precisely the same "the store's native data structure already matches the access pattern" reasoning used for Redis in the Rate Limiter and URL Shortener problems.

L. End-to-End Flow — Fanout-on-Write (Post → Delivery)

sequenceDiagram participant U as Author participant PS as Post Service participant DB as Post DB participant MQ as Message Queue participant FO as Fanout Worker participant Cache as Feed Store (Redis ZSET) U->>PS: POST /posts PS->>DB: write post (source of truth) PS->>MQ: publish "new_post" event PS-->>U: 201 Created MQ->>FO: consume event FO->>FO: fetch follower list loop for each follower FO->>Cache: ZADD user_feed:{follower_id} post_id score end

Why the queue between the write and the fanout: the exact same reasoning as URL Shortener's analytics pipeline — the fanout can be arbitrarily slow (celebrity-scale, per G) and must never sit on the critical path of "did my post succeed." The author gets their 201 the instant the source-of-truth write lands; fanout happens asynchronously afterward.

M. Deep Dive 1 — Fanout Strategy: Write vs. Read vs. Hybrid

Requirement this addresses: the core read/write imbalance from D and G — this is the single most important decision in the whole problem.

StrategyHowRead CostWrite CostFatal Flaw
Fanout-on-read (V1, J)Compute the feed at read time by querying followees' posts liveHigh — a live fan-in query + merge on every loadZeroPuts the most expensive work on the highest-frequency path (reads outnumber writes ~50:1 per G)
Fanout-on-write (chosen default)On post, immediately push the post into every follower's precomputed feedTrivial — one ZREVRANGE readProportional to follower countA celebrity post with 10M followers means 10M writes for one post — can take over an hour (G) and floods the fanout workers
Hybrid (final answer)Fanout-on-write for typical users; fanout-on-read for celebrity posts, merged into the reader's feed only at load timeSlightly more than pure write-fanout — one extra merge step for celebrity contentBounded — celebrities never trigger a massive write stormMore moving parts (two code paths instead of one) — the accepted cost for solving the actual skewed-distribution problem

Why hybrid, not one uniform strategy: the follower-count distribution is the entire crux (E's clarifying question 2) — the two strategies fail at opposite ends of it. Fanout-on-write is clearly right for a user with 200 followers (cheap, and gives their followers a fast read); it is clearly wrong for a user with 10M followers (one post shouldn't trigger 10M synchronous-ish writes). Treating "average follower count is low" as license to use one strategy everywhere is the single most common mistake on this problem — the average hides exactly the tail that breaks the design.

N. Deep Dive 2 — The Celebrity Problem, In Detail

Requirement this addresses: directly extends M — this is where the hybrid strategy's threshold and mechanics get concrete.

flowchart TB P["New Post"] --> Check{"author follower_count
> threshold (e.g. 1M)?"} Check -- "No — normal user" --> FO["Fanout-on-write:
push to all followers' feeds now"] Check -- "Yes — celebrity" --> Skip["Skip write-fanout entirely.
Post only written to author's own post list"] Skip -.-> Read["At read time: merge author's
recent posts into follower's
feed on-the-fly (fanout-on-read)"] FO --> Feeds[("Followers' precomputed feeds")] Read --> Feeds

The threshold check happens once, at post time — not a runtime cost paid on every read for every user.

Why a follower-count threshold, and why it's cheap to check: follower_count is deliberately denormalized onto the User entity (H) specifically so this check is a single field read, not a COUNT(*) over the follow graph on every single post. The threshold itself (Hello Interview and ByteByteGo both use illustrative values in the hundreds-of-thousands-to-low-millions range) is a tuning knob, not a hard constant — the point being tested is recognizing that a threshold-based split is needed, not memorizing the exact number.

The follow/unfollow edge case this creates: a normal user's feed is a static, precomputed list plus a live merge of celebrity content — meaning if a user follows a new celebrity, nothing needs to be backfilled into their feed at all (celebrity content is always merged live); but if a user follows a new *normal* user, the follow event itself must NOT retroactively push that user's back-catalog into the new follower's feed — only new posts going forward. Getting this backfill boundary right (new posts only, never historical) is a common subtlety worth naming explicitly.

O. Deep Dive 3 — The Precomputed Feed/Timeline Store

Requirement this addresses: Latency (D) — the <200ms feed-load target is only achievable if the read is a single cheap lookup, not a computation.

Why one row per (follower, post), not a join: a normalized design would join Follow and Post at read time — exactly V1's mistake, just moved one layer down. The precomputed FeedEntry model deliberately duplicates data (the same post_id appears once per follower) in exchange for making the read O(1)-ish (one sorted-set range query). This is a direct, worthwhile application of denormalize-for-reads, the same trade-off implicit in every cache-aside pattern in the URL Shortener problem, just applied to a primary data structure instead of a cache layer.

Bounding the size: feeds don't need infinite history precomputed — capping each user's stored feed at, say, the most recent ~800 entries (per E's clarifying question 5) bounds memory per user regardless of how prolific their followees are; anything older falls back to a (rarer, acceptable-latency) on-demand fanout-on-read query.

P. Deep Dive 4 — Caching Viral / Hot Posts

Requirement this addresses: a distinct hot-spot problem from N — N is about fanout write cost for celebrity authors; this is about read cost when one specific post goes viral and is fetched by an enormous number of readers nearly simultaneously.

Why normal cache sharding doesn't fix this: a single viral post_id hashes to one shard no matter how many shards exist — identical in shape to the Rate Limiter's hot-key problem (Q in that section), just triggered by read fan-in instead of one abusive writer.

Q. Deep Dive 5 — From Chronological to Ranked Feed

Requirement this addresses: the explicit extension flagged in F/E — replacing "most recent first" with "most relevant first."

Ranking is a separable layer, not a rearchitecture: the precomputed FeedEntry sorted set already supports an arbitrary score, not just a timestamp. Swapping recency for a relevance score — a weighted function of recency, the reader's affinity to the author (past interactions), predicted engagement (likes/replies), and content type — changes what gets written as the score at fanout time, not the storage/delivery mechanism from O.

ApproachHowTrade-off
Pure recency (base design)Score = timestampSimple, fully explainable, but surfaces low-engagement posts equally with high-engagement ones
Heuristic weighted scoreScore = f(recency, affinity, predicted engagement) computed at fanout timeMeaningfully better relevance for modest complexity; still explainable
ML ranking model (real-world default)A trained model scores candidate posts, typically at read time over a smaller candidate set (fanout still narrows the pool first)Best relevance, but a full ML pipeline (feature store, training, serving) is usually out of scope for the interview itself — worth naming as the real-world answer without pretending to design the model in the room

Why fanout still matters even with read-time ranking: ranking needs a bounded candidate set to score — the precomputed feed/inbox from O is exactly what narrows "every post ever" down to "the few hundred candidates worth ranking for this user," so the ranking layer sits on top of the fanout design, not instead of it.

R. Deep Dive 6 — Media Storage & Delivery

Requirement this addresses: the media-support assumption from F — images/video have a completely different size and access profile than post text.

Why media never touches the post DB or feed store: a video attachment can be MB-GB in size — storing it inline would blow out row sizes in a store optimized for small, frequent reads (O). Instead: media uploads go to a blob store (S3-class object storage), the Post row stores only a URL/reference, and reads are served via a CDN fronting the blob store — the same "don't put large binary data in your hot-path database" principle, and the same CDN-for-static-content pattern used for redirects in the URL Shortener problem, just applied to media bytes instead of a redirect response.

S. Deep Dive 7 — Search

Requirement this addresses: the explicit extension flagged in E's clarifying question 4 — a genuinely different access pattern (full-text/keyword) from timeline delivery by follow-graph.

Why the feed store (O) can't serve this: the sorted-set-by-user model is built for "give me this user's feed ordered by score" — it has no notion of matching arbitrary text across all posts globally. Search requires an inverted index (Elasticsearch/OpenSearch-class system), populated asynchronously off the same "new_post" event stream already used for fanout (L) — one write, two independent async consumers (fanout worker, search indexer), each free to fail or lag independently without affecting the other.

T. Full Solution at Scale — Final Architecture

flowchart TB U(["Author"]) --> PS["Post Service"] PS --> PDB[("Post DB — source of truth")] PS --> MQ["Message Queue
(new_post events)"] subgraph FANOUT [" Fanout Path (M, N) "] direction LR Check{"follower_count
> threshold?"} FW["Fanout Worker
(normal users)"] Check -- "no" --> FW end MQ --> Check subgraph STORAGE [" Precomputed Feed Store (O) "] direction LR Redis1[("Feed ZSETs
shard 1")] Redis2[("Feed ZSETs
shard N")] end FW --> STORAGE subgraph ASYNC [" Parallel Async Consumers "] direction LR Search["Search Indexer (S)"] end MQ --> ASYNC Reader(["Reader"]) --> FeedSvc["Feed Service"] FeedSvc --> STORAGE Check -- "yes — celebrity" --> Merge["Live merge of celebrity posts (N)"] FeedSvc -. "read-time merge" .-> Merge FeedSvc -- "rank candidates (Q)" --> Reader PS -.-> Blob["Blob Store + CDN (R)"]

Everything above is the composition of M through S — no new component introduced here.

U. Design Evolution

What stays stable throughout: the GET /v1/feed contract from I and the append-only "new_post" event from L — every version underneath changes without the client noticing.

V. Consistency Model

Deliberately eventually consistent almost everywhere, matching D's NFR directly: a follower seeing a new post a few seconds after it's published (fanout lag) is a non-issue, and a search result lagging the live feed by longer still is equally fine (S). The one place closer-to-immediate consistency matters is the author's own read-after-write view of their own post — a user who just posted expects to see it on their own profile instantly, which is why the author's own timeline reads from the source-of-truth Post DB directly (or a cache populated synchronously at write time), not through the same async fanout path their followers use.

W. Failure Scenarios

X. Bottlenecks & Scaling Boundary

StageActual BottleneckFix Applied
V1 fanout-on-readEvery feed load pays a live fan-in query costFanout-on-write, precomputed store (M, O)
Uniform fanout-on-writeCelebrity posts trigger write storms (10M+ writes)Hybrid threshold-based fanout (N)
Hybrid fanout, single cache tierA viral post overwhelms one shard on the read sideReplicated hot-content caching (P)
Chronological-only feed at scaleRelevance ceiling — recency alone under-serves high-engagement contentRelevance scoring at fanout/read time (Q)

Y. Design Trade-Offs

DecisionOption AOption BTrade-off
Fanout strategyFanout-on-readFanout-on-write + hybrid (chosen)Read is simpler and has zero fanout lag, but fails the read-heavy NFR; write is fast to read but needs a celebrity carve-out
Feed modelNormalized join at read timeDenormalized precomputed FeedEntry (chosen)Join saves storage and avoids duplication; denormalized trades storage for a dramatically cheaper, faster read
RankingPure chronologicalRelevance-scored (extension)Chronological is simple and fully explainable; ranked is better UX at the cost of a scoring layer and reduced transparency
Media storageInline in Post DBBlob store + CDN (chosen)Inline is simpler for tiny attachments; blob+CDN is required once media size/volume is realistic
SearchQuery the feed store directlyDedicated inverted index (chosen)Reusing the feed store avoids a new system but can't do full-text matching at all; a dedicated index is the only correct tool for the access pattern

Z. Common Bad Designs

AA. Interviewer Follow-Ups

Follow-UpDesign ChangeComponents ChangedComponents Unchanged
"A celebrity account has 100M followers — what happens?"Confirms/exercises the hybrid threshold from N directlyNone — this is exactly what N already handlesEverything
"Users complain their feed isn't 'smart' enough."Layer in relevance scoring (Q) on top of the existing fanout/store designScore computation at fanout/read timeStorage, delivery, celebrity handling
"Add the ability to search posts by keyword."Add an async search indexer off the same event stream (S)New indexing consumer + search storeFanout path, feed store
"A post just went viral and is getting hammered."Promote it into the replicated hot-content cache tier (P)Cache replication policySharding, fanout logic
"Support un-posting / deleting a post after the fact."Tombstone at the source of truth; lazily filter out on feed read, or best-effort async removal from precomputed feedsPost DB + a cleanup consumerThe rest of the fanout/read architecture

AB. Interviewer Evaluation Rubric

AC. Interview Simulation

Interviewer: Design a system like Twitter — posting, following, and a feed of who you follow.
Candidate: The core tension here is when to compute the feed. I could query posts from everyone you follow every time you open the feed, or I could push each new post into every follower's feed the moment it's posted. Given that reads massively outnumber writes on a platform like this, I'd lean toward doing the expensive work at write time.
Interviewer: Okay, so fanout-on-write. What happens when someone with 50 million followers posts?
Candidate: That's exactly the failure case of pure fanout-on-write — you'd be triggering 50 million writes for one post, which can take a very long time and floods whatever's doing the fanout. I'd use a hybrid: for normal accounts, fanout-on-write as usual; past some follower-count threshold, skip the write fanout entirely and instead merge that account's recent posts into their followers' feeds live, at read time.
Interviewer: How do you know at post time whether someone's a "celebrity"?
Candidate: I'd keep a denormalized follower count on the user record itself, so it's a single field read, not a count query over the whole follow graph, on every post.
Interviewer: Where does the actual feed live?
Candidate: A precomputed structure per user — I'd use something like a Redis sorted set keyed by user ID, storing post IDs scored by recency, capped at some reasonable history length. Reading a feed is then one range query, not a live join or fan-in.
Interviewer: Now one specific post — not from a celebrity — suddenly goes viral. What breaks?
Candidate: That's a different problem from the celebrity case — that's a hot read on one key, not a write storm. Sharding doesn't help because one post ID always lands on one shard. I'd replicate that specific post's data across multiple cache nodes once it's detected as hot, so read traffic for it is spread out instead of hammering one shard.
Interviewer: Last thing — make the feed ranked instead of chronological.
Candidate: That changes what score I write into the sorted set — instead of pure timestamp, it's some weighted function of recency, how much the reader engages with that author, and predicted engagement. The storage and delivery mechanism don't need to change at all — fanout still narrows down the candidate set, ranking just changes how that set gets ordered.

AD. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
Hello Interview (FB News Feed breakdown)Requirements-first framing, explicit fanout-on-write/read/hybrid comparison, staff-level ranking discussionCleanest articulation of the hybrid threshold as the central decisionUses Facebook framing rather than Twitter specifically, though the fanout problem is identicalThe requirements-driven structure adopted for this whole section
ByteByteGoDetailed fanout worker architecture, media handling, and notification-adjacent concernsMost thorough treatment of the async fanout pipeline's internalsLighter on the specific numeric celebrity-threshold reasoningThe queue-decoupled fanout-worker pipeline shape used in L and T
donnemartin/system-design-primer (GitHub)General scaling primer with a dedicated fanout-service discussion applicable directly to feed systemsGood grounding in the general read-heavy caching/queue patterns reused throughout this sectionNot Twitter-specific — requires translating general scaling advice into the feed domainThe general "queue between write and expensive fanout" pattern, reused identically from the Rate Limiter's analytics discussion
Redis documentation (sorted sets)Native ZADD/ZREVRANGE semantics used directly for the feed storeConcrete, correct primitive for exactly this access patternNo system-design framing — implementation reference onlyWhy a sorted set's native score-ordering maps directly onto "give me my feed, most recent/relevant first"

AE. Interview Cheat Sheet

ProblemTwitter / News Feed
DifficultyIntermediate → Advanced
Core requirementsPost, follow, reverse-chron feed; 200M DAU, ~50:1 read:write ratio, <200ms feed load, eventual consistency OK
Central decisionFanout-on-write (cheap reads) vs. fanout-on-read (cheap writes) — resolved via a hybrid, threshold-based split
Celebrity fixSkip write-fanout above a follower-count threshold; merge their posts live at read time instead
Feed storePrecomputed, capped, sorted-set "inbox" per user (denormalized, one entry per follower per post)
Viral-post fixReplicate hot content across cache nodes — sharding alone can't spread one key's read load
RankingA scoring layer on top of the same fanout/store mechanism — not a rearchitecture
Media & searchDelegated to a blob store+CDN and an async inverted index, respectively — both off the same event stream
Common mistakesUniform fanout-on-write with no celebrity carve-out; normalized read-time joins; treating ranking as day-one scope
What NOT to over-engineerA full ML ranking pipeline in the room — name it as the real-world answer, don't design the model