3. Twitter / News Feed Intermediate → Advanced
A. Realistic Interview Prompt
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
- Whether you independently arrive at "fanout-on-write vs. fanout-on-read" as the central trade-off, rather than needing it spelled out
- Recognition that a single strategy fails at the tails of a skewed distribution (celebrities), and the judgment to apply a hybrid rather than forcing one strategy everywhere
- Read/write ratio reasoning — feeds are read orders of magnitude more than posts are written, so the design should shift work toward write time wherever the fanout width allows it
- Comfort with a precomputed, denormalized "inbox" data model (one entry per follower per post) instead of reflexively normalizing everything
- Knowing where ranking, search, and media serving are separable concerns layered on top of the feed-delivery core, not part of it
D. Requirements
Functional
- Users can post short text updates (optionally with media) and follow/unfollow other users
- Users can view a feed composed of posts from people they follow, ordered by recency (baseline) or relevance (extension, Q)
- Users can like, reply to, and repost/retweet content
Non-Functional
- Read-heavy at extreme scale — feed reads vastly outnumber posts written; this document uses 200M DAU, each checking their feed ~5×/day and posting ~10% of the time as the standard illustrative input, deriving all downstream QPS from it in G
- Low latency feed loads — a feed should render in <200ms; this is a "check my phone" interaction, not a batch job
- Eventual consistency is acceptable — a new post appearing in a follower's feed a few seconds late is a non-issue; this is the single assumption that makes async fanout tractable
- Highly available for reads — a feed failing to load is a much worse user experience than a post being delayed by a few seconds
Out of scope: direct messages, notifications infrastructure, ad insertion, detailed content moderation — each is its own design problem.
E. Clarifying Questions
| # | Question | Why It Matters | Design Decision Affected |
|---|---|---|---|
| 1 | Chronological feed, or ranked/algorithmic? | Chronological is a pure delivery problem; ranked adds a scoring/ML layer on top | Deep Dive 5 (Q) |
| 2 | What'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 strategy | Deep Dive 1-2 (M, N) |
| 3 | How stale can a feed be before it's a problem? | Confirms eventual consistency is acceptable, unlocking async fanout entirely | Consistency Model (V) |
| 4 | Is search (by keyword/hashtag) in scope? | A materially different data-access pattern (full-text) from timeline delivery | Deep Dive 7 (S) |
| 5 | How far back does "the feed" need to go — infinite scroll, or a bounded recent window? | Bounds the size of the precomputed inbox structure in O | Timeline Store (O) |
| 6 | Do posts support media (images/video)? | Changes storage and delivery entirely — blob store + CDN, not the feed DB | Deep Dive 6 (R) |
F. Assumptions
- Confirmed: post, follow, and reverse-chronological feed read are the core functional loop.
- Assumption: the social graph has real celebrity accounts with 10M+ followers — the standard, most-interesting version of this problem, and what every major source (Hello Interview, ByteByteGo, the donnemartin repo) converges on.
- Assumption: chronological feed is the base design (J-T); ranking is an explicit, clearly-flagged extension (Q), not baked in from the start — mirrors Rate Limiter's structure of "solve the core problem, then layer the harder extension."
- Assumption: eventual consistency for feed delivery (a few seconds of fanout lag is fine) — this is the load-bearing assumption for the entire async design.
- Assumption: media is supported but delegated to a separate blob-store + CDN path (R), not stored in the feed/post database itself.
G. Capacity Estimation
| Metric | Value / Calculation | Why It Matters |
|---|---|---|
| DAU | 200M | Base input for every number below |
| Posts/day | 200M × 10% ≈ 20M/day | Write volume — small relative to reads |
| Post write QPS | 20M ÷ 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/day | 200M × 5 ≈ 1B/day | Read volume dwarfs write volume by ~50× |
| Feed read QPS | 1B ÷ 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 writes | At 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/year | 20M/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
| Entity | Purpose | Key Fields |
|---|---|---|
User | An account | user_id, username, follower_count (denormalized — read constantly, see N) |
Post | Source of truth for one post | post_id, author_id, text, media_url, created_at |
Follow | Edge in the social graph | follower_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.
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)
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.
| Strategy | How | Read Cost | Write Cost | Fatal Flaw |
|---|---|---|---|---|
| Fanout-on-read (V1, J) | Compute the feed at read time by querying followees' posts live | High — a live fan-in query + merge on every load | Zero | Puts 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 feed | Trivial — one ZREVRANGE read | Proportional to follower count | A 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 time | Slightly more than pure write-fanout — one extra merge step for celebrity content | Bounded — celebrities never trigger a massive write storm | More 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.
> 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.
- Full replication for hot content — instead of sharding a viral post's data, replicate it across every cache node (or a dedicated small pool of nodes), so read traffic for that one post is spread across all of them rather than hammering one shard.
- Local, short-TTL in-process caching at the edge/API-server layer — a post that's hot right now is still hot 500ms from now; a very short local cache absorbs a large fraction of read fan-in before it ever reaches the shared cache tier.
- Detecting "hot" dynamically — track access frequency per key and promote a post into the replicated tier automatically once it crosses a threshold, rather than guessing in advance which posts will go viral.
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.
| Approach | How | Trade-off |
|---|---|---|
| Pure recency (base design) | Score = timestamp | Simple, fully explainable, but surfaces low-engagement posts equally with high-engagement ones |
| Heuristic weighted score | Score = f(recency, affinity, predicted engagement) computed at fanout time | Meaningfully 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
(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
- V1 — fanout-on-read (J): correct, but puts the expensive merge on the highest-frequency path.
- V2 — fanout-on-write for everyone: fixes the read-cost problem, but a single celebrity post now triggers a write storm (M).
- V3 — hybrid fanout with a follower-count threshold: solves the celebrity write-storm problem (M, N).
- V4 — precomputed, capped, sorted-set feed store: hits the <200ms read target (O).
- V5 — replicated caching for viral posts: handles read fan-in on one hot post that write-side sharding alone can't (P).
- V6 — relevance scoring at fanout time: upgrades chronological to ranked without changing the delivery mechanism (Q).
- V7 — blob store + CDN for media: keeps large binary content out of the hot-path stores (R).
- V8 — async search indexer off the same event stream: adds full-text search without coupling it to the fanout path (S).
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
- A fanout worker crashes mid-fanout: the message queue's redelivery/at-least-once semantics ensure the event is reprocessed; fanout writes (ZADD) are naturally idempotent, so reprocessing is safe (L).
- The feed store shard for a hot user goes down: that user's reads degrade to a fallback fanout-on-read query against the Post DB — slower, but available, not a hard outage (O, N).
- A celebrity post's live merge is slow: bound the celebrity-merge window (e.g. only their last N posts) so a slow merge degrades gracefully rather than blocking the whole feed load (N).
- The search indexer falls behind: isolated entirely to the search path — feed delivery is unaffected, since it's a fully independent consumer of the same event stream (S).
- A write storm from an unexpectedly-viral normal-user post (follower count under the celebrity threshold, but the post itself goes viral): handled by P's read-side hot-key mitigations, not by N's write-side threshold, since N only protects against a large existing follower base, not an unpredictable spike in engagement.
X. Bottlenecks & Scaling Boundary
| Stage | Actual Bottleneck | Fix Applied |
|---|---|---|
| V1 fanout-on-read | Every feed load pays a live fan-in query cost | Fanout-on-write, precomputed store (M, O) |
| Uniform fanout-on-write | Celebrity posts trigger write storms (10M+ writes) | Hybrid threshold-based fanout (N) |
| Hybrid fanout, single cache tier | A viral post overwhelms one shard on the read side | Replicated hot-content caching (P) |
| Chronological-only feed at scale | Relevance ceiling — recency alone under-serves high-engagement content | Relevance scoring at fanout/read time (Q) |
Y. Design Trade-Offs
| Decision | Option A | Option B | Trade-off |
|---|---|---|---|
| Fanout strategy | Fanout-on-read | Fanout-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 model | Normalized join at read time | Denormalized precomputed FeedEntry (chosen) | Join saves storage and avoids duplication; denormalized trades storage for a dramatically cheaper, faster read |
| Ranking | Pure chronological | Relevance-scored (extension) | Chronological is simple and fully explainable; ranked is better UX at the cost of a scoring layer and reduced transparency |
| Media storage | Inline in Post DB | Blob store + CDN (chosen) | Inline is simpler for tiny attachments; blob+CDN is required once media size/volume is realistic |
| Search | Query the feed store directly | Dedicated 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
- Fanout-on-write for every user with no celebrity carve-out. Works in a demo with a few hundred users; falls over the instant one account has millions of followers — the single most common mistake on this problem.
- Fanout-on-read presented as "the" solution with no mention of the read/write ratio from G — technically correct, but ignores the NFR that makes this problem interesting in the first place.
- Normalizing the feed as a join between Follow and Post tables computed at read time — the same V1 mistake, just moved one layer into "the database" instead of "the application," with the same latency consequence.
- Treating ranking as a day-one requirement instead of a layer on top of a working chronological design — leads to spending the whole interview on a scoring model instead of the delivery architecture that actually carries most of the signal.
- Storing media inline in the post/feed tables. Bloats the exact tables that need to stay small and fast for the core read path.
AA. Interviewer Follow-Ups
| Follow-Up | Design Change | Components Changed | Components Unchanged |
|---|---|---|---|
| "A celebrity account has 100M followers — what happens?" | Confirms/exercises the hybrid threshold from N directly | None — this is exactly what N already handles | Everything |
| "Users complain their feed isn't 'smart' enough." | Layer in relevance scoring (Q) on top of the existing fanout/store design | Score computation at fanout/read time | Storage, 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 store | Fanout path, feed store |
| "A post just went viral and is getting hammered." | Promote it into the replicated hot-content cache tier (P) | Cache replication policy | Sharding, 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 feeds | Post DB + a cleanup consumer | The rest of the fanout/read architecture |
AB. Interviewer Evaluation Rubric
- Weak candidate: "query posts from people you follow, sort by date" with no mention of precomputation, no awareness of the read/write imbalance.
- Mid-level: proposes fanout-on-write with a precomputed feed store; hasn't yet considered what happens with a very large follower count.
- Senior: proactively raises the celebrity/skewed-distribution problem and proposes a hybrid threshold-based fanout unprompted; separates ranking and search as layered extensions rather than baking them into the core design.
- Staff+: discusses the viral-post hot-key read problem distinctly from the celebrity write-fanout problem; reasons concretely about consistency (author read-after-write vs. follower eventual consistency) rather than asserting "eventually consistent" as a blanket statement.
AC. Interview Simulation
AD. Online Solution Comparison
| Source | Approach | Strength | Weakness | Worth Learning |
|---|---|---|---|---|
| Hello Interview (FB News Feed breakdown) | Requirements-first framing, explicit fanout-on-write/read/hybrid comparison, staff-level ranking discussion | Cleanest articulation of the hybrid threshold as the central decision | Uses Facebook framing rather than Twitter specifically, though the fanout problem is identical | The requirements-driven structure adopted for this whole section |
| ByteByteGo | Detailed fanout worker architecture, media handling, and notification-adjacent concerns | Most thorough treatment of the async fanout pipeline's internals | Lighter on the specific numeric celebrity-threshold reasoning | The 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 systems | Good grounding in the general read-heavy caching/queue patterns reused throughout this section | Not Twitter-specific — requires translating general scaling advice into the feed domain | The 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 store | Concrete, correct primitive for exactly this access pattern | No system-design framing — implementation reference only | Why a sorted set's native score-ordering maps directly onto "give me my feed, most recent/relevant first" |
AE. Interview Cheat Sheet
| Problem | Twitter / News Feed |
| Difficulty | Intermediate → Advanced |
| Core requirements | Post, follow, reverse-chron feed; 200M DAU, ~50:1 read:write ratio, <200ms feed load, eventual consistency OK |
| Central decision | Fanout-on-write (cheap reads) vs. fanout-on-read (cheap writes) — resolved via a hybrid, threshold-based split |
| Celebrity fix | Skip write-fanout above a follower-count threshold; merge their posts live at read time instead |
| Feed store | Precomputed, capped, sorted-set "inbox" per user (denormalized, one entry per follower per post) |
| Viral-post fix | Replicate hot content across cache nodes — sharding alone can't spread one key's read load |
| Ranking | A scoring layer on top of the same fanout/store mechanism — not a rearchitecture |
| Media & search | Delegated to a blob store+CDN and an async inverted index, respectively — both off the same event stream |
| Common mistakes | Uniform fanout-on-write with no celebrity carve-out; normalized read-time joins; treating ranking as day-one scope |
| What NOT to over-engineer | A full ML ranking pipeline in the room — name it as the real-world answer, don't design the model |