Capstone — putting the patterns together
Eighteen lessons gave you a toolbox. An interview gives you 45 minutes and one ambiguous prompt. This lesson is the bridge: one design, walked end to end, where every pattern is reached for on purpose and every trade defended out loud. The skill being graded is not "do you know Redis" — it is "can you derive the right shape from numbers and then own its failure modes."
Step 1 — Requirements & SLOs (the method, lesson 01)
Open every interview by pinning down what before how. Restate the prompt, then split functional from non-functional and narrow aggressively — a senior cuts scope, a junior tries to build all of Twitter in 45 minutes.
Functional — and what we cut
- In scope: post a tweet; follow / unfollow a user; read your home timeline (the merged, reverse-chronological-ish feed of people you follow). This is the heart that touches every pattern.
- Stretch, only if time: likes / retweets (just more write events), search.
- Explicitly cut: DMs, notifications, media transcoding, ML ranking internals, ads, trends. Say this out loud — "I'll treat ranking as a pluggable step and assume reverse-chronological for the core; we can layer a ranker later." Cutting scope is the senior move.
Non-functional — turn adjectives into SLOs (lesson 02, 09, 13)
| Property | Target | Why this number / which lesson |
|---|---|---|
| Read/write ratio | ~100 : 1, read-heavy | People read far more than they post. This single fact dictates the whole architecture — optimize the read path. |
| Timeline read latency | P99 < 200 ms | Perceived-instant feed (02). P99, not mean — tail latency is the user experience. |
| Post (write) latency | P99 < 500 ms to accept | Accepting the tweet is fast; full propagation is async (11). Decouple accept from fan-out. |
| Consistency | Eventual for the feed; read-your-writes for self | It is fine if a followee's tweet shows up a few seconds late (09). It is not fine if your own tweet vanishes after you post it. |
| Availability | Degrade, never hard-fail reads | A slightly stale timeline beats an error page (13). Reads must survive partial outages. |
Step 2 — Estimates: let the numbers pick the architecture (lessons 01/02)
Back-of-envelope before boxes. Pick round numbers, state them, and compute. The goal is an order of magnitude, not precision.
| Quantity | Assumption | Derivation | Result |
|---|---|---|---|
| Users / DAU | 300M total, 150M daily active | given | 150M DAU |
| Avg followers | 200 (heavy-tailed!) | median ≪ mean; a few accounts have 10–100M | 200 avg, tail to 100M |
| Write QPS | 2 posts/user/day | 150M × 2 / 86,400 s | ≈ 3,500 writes/s avg |
| Read QPS | 20 timeline reads/user/day | 150M × 20 / 86,400 s | ≈ 35,000 reads/s avg |
| Peak factor | ~3× diurnal peak | multiply both by 3 | ≈ 10.5K writes/s, 105K reads/s peak |
| Tweet storage / yr | ~300 bytes/tweet | 3,500/s × 86,400 × 365 × 300 B | ≈ 110M tweets/day → ~33 TB/yr |
| Fan-out volume | posts × avg followers | 3,500 posts/s × 200 followers | ≈ 700K timeline writes/s avg |
- The system is read-heavy → optimize reads → precompute (push) is attractive: a read becomes one cache lookup.
- But it is also fan-out-dominated on the write side once we push, and the heavy-tailed graph means a single celebrity post = 1 × 50,000,000 = 50M timeline writes — a thunderclap that dwarfs the steady 700K/s.
Step 3 — API & data model
Sketch a tiny, honest API surface. Three endpoints carry the whole design.
POST /v1/tweets { text } → { tweet_id, ts } // post
GET /v1/timeline?cursor= & limit=50 → { tweets[], next_cursor }
POST /v1/follow { target_user_id } → 204
Two small contract choices worth saying out loud (03). First, POST /v1/tweets takes an idempotency key (a client-generated id, sent as a header) so that a retry after a dropped response doesn't double-post — the server records the key and returns the original tweet_id on a repeat rather than creating a second tweet. Creates are not naturally idempotent, and on a flaky mobile network retries are the norm, so this is the difference between "post a tweet" and "post a tweet, sometimes twice" (the same at-least-once reasoning as the fan-out path in 12). Second, the /v1/ prefix buys forward-compatible evolution: when the response shape eventually has to change incompatibly, you ship /v2/ alongside and migrate clients at their own pace instead of breaking every old app at once.
Data model — four objects
The load-bearing schema choice here is the materialised timeline — a denormalised, precomputed copy of the feed that trades extra write work and storage for an O(1) read, exactly the denormalisation-for-read-speed trade weighed in 04.
| Object | Shape | Notes / lesson |
|---|---|---|
| users | user_id (PK), handle, profile, follower_count | follower_count drives the celebrity routing decision. |
| tweets | tweet_id (Snowflake: time-sortable), author_id, text, ts | Partitioned by author_id (07). ID encodes time → sortable without a separate index. |
| follow graph | adjacency: followers(user_id)→[…], following(user_id)→[…] | Two directions, both needed: following for pull, followers for push fan-out. |
| timeline (materialized) | per-user list of (tweet_id, ts), capped to ~800 entries | The precomputed home feed. Lives in the cache/store. Capped — nobody scrolls 10K deep. |
WHERE tweet_id < cursor ORDER BY tweet_id DESC LIMIT 50), never OFFSET n. Offset scans and discards n rows — O(n) and it skips/duplicates rows when the feed mutates under you. A keyset cursor (the last seen Snowflake ID) is O(log n) on the index and stable under inserts. Snowflake IDs being time-sortable is what makes the cursor "just" the last id.Step 4 — The central decision: fan-out on write vs read
This is the load-bearing question for any feed. There are two pure strategies and one correct answer.
| Fan-out on WRITE (push) | Fan-out on READ (pull) | |
|---|---|---|
| At post time | Write the tweet id into every follower's materialized timeline. | Just store the tweet under the author. Cheap. |
| At read time | One lookup of the user's precomputed timeline. O(1), fast. | Scatter-gather: query everyone you follow, merge-sort by ts. O(following). |
| Write cost | O(followers) — brutal for celebrities. | O(1). |
| Read cost | O(1) cache hit. | O(following) scatter-gather, then merge — slow, and a fan-in hotspot (07). |
| Best when | Read-heavy, modest fan-out. | Write-heavy, or accounts with enormous fan-out. |
Pure push is great for our 100:1 read ratio — until a celebrity posts. 1 post × 50M followers = 50M timeline writes for a single tweet, all hammering the same fan-out pipeline and the same hot shards. That is the hot-key problem (07) and a availability bomb (13) in one. Pure pull avoids it but makes the common case — an ordinary user reading their feed — a scatter-gather over hundreds of followees on every single read.
Step 5 — Map every pattern onto the design (the payoff)
Now the lessons snap into place. Walk the request path and name each one as you reach for it — and say why.
- Alice
POSTs a tweet. The tweet row and an outbox row are written in ONE local transaction (12) on the tweet store; that commit is all she waits on, soPOST /tweetreturns to her in well under 500 ms. - The relay reads the committed outbox row and publishes a fan-out event to the durable log / queue (11).
- A fan-out worker consumes the event, checks Alice's
follower_countis below the celebrity threshold T, and appends the tweet id to each follower's materialised timeline in the timeline store. The write is idempotent on (user_id, tweet_id), so an at-least-once redelivery of the same event is a harmless no-op. - Bob, one of Alice's followers, later issues
GET /timeline. The timeline service serves his precomputed timeline from the distributed cache (06) as a single hit. - That same read merges in any celebrity tweets Bob follows — pulled on demand at read time and heavily cached — completing the hybrid.
- Bob sees Alice's tweet a few seconds after step 1. That lag IS the eventual-consistency budget we chose back in Step 1's requirements — spent deliberately, not by accident.
Pattern-by-pattern, with the defense
- Edge — gateway + rate limiting (15): one front door does TLS, authn, routing, and admission control. Token-bucket per user/IP keeps the backend below the saturation cliff and stops scrapers cheaply. Trade: the gateway is a critical-path hop, so it must itself be replicated and fast.
- API tier — stateless behind a load balancer (05): no session affinity, so we scale horizontally and any replica serves any request. Trade: all state pushed down to cache/store.
- Cache (06): materialized home timelines and hot tweets live in a distributed cache; a read is ideally one hop. Defend the failure mode: stampede protection (request coalescing / single-flight + jittered TTLs) so a popular celebrity tweet expiring doesn't trigger a thundering herd to the store.
- Partitioning (07): tweets and timelines sharded by user_id via consistent hashing → add nodes with minimal reshuffle. Caveat stated up front: a celebrity is a hot shard; the hybrid (not pushing celebrities) is precisely the mitigation, plus we can replicate/duplicate hot keys.
- Replication (08): stores are leader-follower replicated with read replicas to absorb the 100:1 read load. Trade named: replication lag → a follower may not have your just-posted tweet. We handle read-your-writes (06/07) by serving the author's own fresh tweet from a write-through fast path / sticky-to-leader read for self, so you always see what you just posted.
- Consistency (09): the feed is eventually consistent by design — a followee's tweet may arrive seconds late and that is fine. We spent strong consistency only where it matters (self-reads, unique handles).
- Async messaging (11): fan-out runs off a durable log/queue so
POST /tweetreturns in <500 ms while timelines populate in the background. The queue is also the shock absorber for the celebrity thunderclap and diurnal peaks — it buffers, workers drain at a safe rate. - Distributed transactions / dual-write (12): writing the tweet to the store and publishing the fan-out event is a dual write. Use the outbox pattern — write the tweet and an outbox row in one local transaction, then a relay publishes the event. Delivery is at-least-once, so timeline writes must be idempotent (keyed on (user_id, tweet_id); a duplicate is a no-op). No 2PC on the hot path.
- Consensus (10): used only where truly needed — unique-handle assignment and the shard-map / config in etcd or ZooKeeper. Never on the feed read/write hot path; consensus is a latency tax you pay only for the rare strongly-consistent decision.
- Fault tolerance (13): degrade, don't fail. If the ranker/feature path is sick, serve the plain reverse-chronological cached timeline or a popularity fallback. Circuit breakers + backoff on every downstream. Multi-region active-active for the read path so a lost region costs latency, not availability.
- Observability (16): SLO on timeline-read P99 with an error budget that gates releases; distributed tracing follows a tweet from POST through the queue into every follower's timeline so you can find where fan-out lag accrues.
Step 6 — Failure analysis (attack your own design)
This is what separates levels. Walk concrete failures and give the answer for each.
| What breaks | What happens | The answer (lesson) |
|---|---|---|
| Cache tier dies | Timeline reads miss en masse → stampede onto the store. | Degrade to reading the tweet store / rebuilding from author tweets; single-flight + jittered TTLs + a small in-process cache cushion the herd (06/13). |
| A region is lost | All traffic to that region's stack fails. | Active-active read path: DNS/anycast sheds to surviving regions; RPO = cross-region replication lag (08/13). |
| Fan-out message delivered twice | A tweet could appear twice in a timeline. | Idempotent timeline writes keyed on (user_id, tweet_id) — the duplicate is a no-op (12). |
| A celebrity posts | 50M push writes would melt the fan-out pipeline and hot shards. | Hybrid: celebrity tweets are not pushed; they are pulled+merged at read time and cached (07/hybrid). |
| Fan-out workers fall behind | Timelines lag; some followers see the tweet late. | Acceptable by SLO (eventual consistency); queue buffers, autoscale workers, alert on fan-out lag (11/16). |
| Store leader fails | Writes stall for that shard. | Quorum failover promotes a follower; writes resume; fence the old leader to avoid split-brain (08/10). |
Step 7 — The reusable checklist (run this in any interview)
Memorise this ordered spine. It is the same method from lesson 01, generalized so you can drive any prompt — URL shortener, chat, rate limiter, ride-share — with the same discipline.
- Requirements — restate; split functional / non-functional; narrow scope explicitly.
- Estimates — QPS (avg + peak), storage, and the dominant cost driver (here: fan-out).
- API — a few endpoints; the contract before the boxes.
- Data model — entities, keys, access patterns; choose the partition key now.
- High-level design — draw the boxes and the request path.
- Scale the read path — cache, replicas, precompute (read-heavy systems live or die here).
- Scale the write path — partition, async, batch; find the write amplification.
- State the consistency choice — where eventual is fine, where it is not; defend it.
- Failure modes — kill each component out loud; give the degradation answer.
- Bottleneck deep-dive — go deep on the one thing that matters (the hot key / celebrity).
- Trade-off summary — close by naming what you optimized for and what you gave up.
Senior moves vs junior tells
| Senior move | Junior tell |
|---|---|
| Drives the design from numbers (fan-out = 700K/s → push). | Name-drops tech ("we'll use Kafka and Redis") with no load to justify it. |
| States the trade-off for every choice, unprompted. | Lists components as if more boxes = better design. |
| Designs the failure path and the degradation ladder. | Only describes the happy path; freezes when asked "what if X dies?" |
| Narrows scope, then goes deep on the bottleneck. | Tries to build everything shallowly; never reaches the hot key. |
| Spends strong consistency only where it's needed. | Reaches for transactions / consensus everywhere "to be safe." |
| Treats the interviewer as a collaborator; checks assumptions. | Monologues a memorized "Twitter design" regardless of the prompt. |
Interactive: fan-out strategy calculator
This is the one decision the whole design pivots on. Move the sliders and watch the crossover between push (write amplification) and pull (read amplification), and the celebrity write-amplification number that forces the hybrid.
Interview prompts you should be ready for
- Walk me through designing a Twitter home timeline. (senior answer: run the spine — restate & narrow scope; estimate QPS/storage/fan-out; tiny API; four-object data model; high-level boxes; then the central fan-out decision; map cache/partition/replicate/async; close with failure modes and the trade-off summary. Drive from the 100:1 read ratio and the 700K/s fan-out.)
- Fan-out on write or on read — and why? (senior answer: neither alone. Push gives O(1) reads (great for 100:1 read-heavy) but O(followers) writes; pull gives O(1) writes but scatter-gather reads. A celebrity makes pure push catastrophic — 50M writes per post. The hybrid pushes below a follower threshold and pulls+merges celebrities at read time.)
- A celebrity with 50M followers posts. What happens and how do you handle it? (senior answer: pure push = 50M timeline writes for one tweet — a hot-shard, hot-pipeline thunderclap (05/11). Don't push celebrities; pull their recent tweets at read time and merge into each reader's precomputed timeline, cached aggressively. The threshold T is a tunable knob set from the follower distribution.)
- How do you keep timeline reads under 200 ms P99 when the store is replicated? (senior answer: serve from a distributed cache of materialized timelines (one hop), backed by read replicas. Defend tail latency with stampede protection and jittered TTLs. Replication lag is fine for followees (eventual) but handle read-your-writes for self via a write-through fast path / leader read.)
- You have a dual write — tweet store plus fan-out event. How do you avoid losing or duplicating? (senior answer: outbox pattern (12) — tweet + outbox row in one local txn, a relay publishes the event, so no 2PC. Delivery is at-least-once, so make timeline writes idempotent keyed on (user_id, tweet_id); duplicates are no-ops.)
- Where, if anywhere, do you need consensus? (senior answer: only off the hot path — unique-handle assignment and the shard-map/config in etcd/ZK (10). The feed read/write path uses none; consensus is a latency and availability tax paid only for rare strongly-consistent decisions.)
- The ranking/feature service starts timing out. What does the user see? (senior answer: nothing broken — circuit breaker opens, we degrade to the plain cached reverse-chronological timeline or a popularity fallback (13). A slightly worse feed beats an error. SLO error budget and tracing (16) tell us it happened and where.)
- What did you optimize for, and what did you give up? (senior answer: optimized read latency and availability for a read-heavy, fan-out-dominated workload; bought it with eventual consistency on the feed, async fan-out complexity, idempotency machinery, and a celebrity-threshold knob to tune. That trade is correct for a social feed and would be wrong for, say, a bank ledger.)