all_lessons / system_design / 19 · capstone lesson 19 / 19

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."

First principle
A senior design interview is a controlled derivation, not a recall quiz. You are given a fuzzy goal and a clock. The signal the interviewer wants: do you narrow scope, compute the load, let the numbers pick the architecture, name each trade-off, and then attack your own design with failure scenarios. Components are cheap to list; the value is in the reasoning between them. We will design a Twitter-like home timeline because it exercises nearly every lesson — read-heavy load, a hot-key celebrity problem, fan-out, eventual consistency, async pipelines, and a brutal failure surface.

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

Non-functional — turn adjectives into SLOs (lesson 02, 09, 13)

PropertyTargetWhy this number / which lesson
Read/write ratio~100 : 1, read-heavyPeople read far more than they post. This single fact dictates the whole architecture — optimize the read path.
Timeline read latencyP99 < 200 msPerceived-instant feed (02). P99, not mean — tail latency is the user experience.
Post (write) latencyP99 < 500 ms to acceptAccepting the tweet is fast; full propagation is async (11). Decouple accept from fan-out.
ConsistencyEventual for the feed; read-your-writes for selfIt 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.
AvailabilityDegrade, never hard-fail readsA slightly stale timeline beats an error page (13). Reads must survive partial outages.
The line that earns the role
"Eventual consistency is acceptable for the timeline, so I will not pay for global coordination on the hot path. That single decision is what lets the read path scale." You have now used CAP / PACELC (09) as a budget, not a buzzword — you spent strong consistency where it does not matter to buy latency where it does.

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.

QuantityAssumptionDerivationResult
Users / DAU300M total, 150M daily activegiven150M DAU
Avg followers200 (heavy-tailed!)median ≪ mean; a few accounts have 10–100M200 avg, tail to 100M
Write QPS2 posts/user/day150M × 2 / 86,400 s≈ 3,500 writes/s avg
Read QPS20 timeline reads/user/day150M × 20 / 86,400 s≈ 35,000 reads/s avg
Peak factor~3× diurnal peakmultiply both by 3≈ 10.5K writes/s, 105K reads/s peak
Tweet storage / yr~300 bytes/tweet3,500/s × 86,400 × 365 × 300 B≈ 110M tweets/day → ~33 TB/yr
Fan-out volumeposts × avg followers3,500 posts/s × 200 followers≈ 700K timeline writes/s avg
Worked estimate — read the verdict off the numbers
Read QPS (35K) is 10× the write QPS (3.5K). But the fan-out — if we precompute timelines on write — is 700K writes/s, ~200× the raw write rate and ~20× the read rate. So: The numbers have already told us the answer: push for the masses, but never push for celebrities. That is the hybrid, and we derived it rather than recalled it.

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.

ObjectShapeNotes / lesson
usersuser_id (PK), handle, profile, follower_countfollower_count drives the celebrity routing decision.
tweetstweet_id (Snowflake: time-sortable), author_id, text, tsPartitioned by author_id (07). ID encodes time → sortable without a separate index.
follow graphadjacency: 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 entriesThe precomputed home feed. Lives in the cache/store. Capped — nobody scrolls 10K deep.
Pagination: keyset, not offset
Use cursor / keyset pagination (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 timeWrite the tweet id into every follower's materialized timeline.Just store the tweet under the author. Cheap.
At read timeOne lookup of the user's precomputed timeline. O(1), fast.Scatter-gather: query everyone you follow, merge-sort by ts. O(following).
Write costO(followers) — brutal for celebrities.O(1).
Read costO(1) cache hit.O(following) scatter-gather, then merge — slow, and a fan-in hotspot (07).
Best whenRead-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.

The senior answer: HYBRID
Push for normal users, pull for celebrities. When you post, fan out to followers only if your follower_count is below a threshold T (say 100K). Accounts above T are "celebrities" — their tweets are not pushed; instead, at read time you take a user's precomputed (push) timeline and merge in the recent tweets of the few celebrities they follow, pulled on demand and heavily cached. Best of both: O(1)-ish reads for everyone, and no 50M-write thunderclap. The cost is read-path complexity (a merge) and the threshold becoming a tuning knob you must own. Defend the knob: "T trades fan-out write amplification against read-merge cost; I'd set it from the follower-count distribution so the number of celebrity accounts stays small enough that the read-side pull set is tiny."

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.

┌─────────────────────────────────────────────────────────┐ client ──TLS──▶ API GATEWAY (15) stateless API tier (05) │ rate-limit, authn, ──▶ behind L7 load balancer ───┐ │ route │ │ ▼ │ READ PATH ┌────────────────────────┐ │ GET /timeline ─────────────────────────────────────▶│ Timeline service │ │ │ 1. read cache (06) │ │ ┌────────────────────────────────┤ 2. merge celeb pulls │ │ │ distributed CACHE (06) │ 3. read-your-writes │ │ │ home timelines + hot tweets │ fast path (06/07) │ │ │ stampede defenses └───────────┬────────────┘ │ └─────────────────────────────────────────────│ │ ▼ │ WRITE PATH ┌──────────────────────────┐ │ POST /tweet ──▶ Tweet service ──write──▶ TWEET STORE│ sharded by author_id (07)│ │ │ │ replicated, read replicas│ │ │ outbox row in same txn (12) │ (08) │ │ ▼ └──────────────────────────┘ │ ASYNC FAN-OUT (11): append to log/queue ──▶ fan-out workers ──▶ TIMELINE STORE │ if author.followers < T: push to each follower's timeline (sharded 05 + │ else: skip (celebrity → pulled at read) idempotent writes replicated 06) │ │ CONFIG / coordination: etcd/ZK (10) holds shard map + unique-handle locks ─────────┘ OBSERVABILITY (16): SLOs on timeline-read P99, error budget, distributed trace of fan-out
Trace one tweet, end to end
  1. 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, so POST /tweet returns to her in well under 500 ms.
  2. The relay reads the committed outbox row and publishes a fan-out event to the durable log / queue (11).
  3. A fan-out worker consumes the event, checks Alice's follower_count is 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.
  4. 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.
  5. That same read merges in any celebrity tweets Bob follows — pulled on demand at read time and heavily cached — completing the hybrid.
  6. 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

Step 6 — Failure analysis (attack your own design)

This is what separates levels. Walk concrete failures and give the answer for each.

What breaksWhat happensThe answer (lesson)
Cache tier diesTimeline 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 lostAll 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 twiceA 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 posts50M 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 behindTimelines 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 failsWrites 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.

  1. Requirements — restate; split functional / non-functional; narrow scope explicitly.
  2. Estimates — QPS (avg + peak), storage, and the dominant cost driver (here: fan-out).
  3. API — a few endpoints; the contract before the boxes.
  4. Data model — entities, keys, access patterns; choose the partition key now.
  5. High-level design — draw the boxes and the request path.
  6. Scale the read path — cache, replicas, precompute (read-heavy systems live or die here).
  7. Scale the write path — partition, async, batch; find the write amplification.
  8. State the consistency choice — where eventual is fine, where it is not; defend it.
  9. Failure modes — kill each component out loud; give the degradation answer.
  10. Bottleneck deep-dive — go deep on the one thing that matters (the hot key / celebrity).
  11. Trade-off summary — close by naming what you optimized for and what you gave up.

Senior moves vs junior tells

Senior moveJunior 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.

Fan-out: push vs pull vs hybrid
Per day, per system of one "average user" scaled to the whole graph by symmetry. Push (fan-out-on-write) cost = posts/user/day × avg followers = timeline writes generated per posting user — the write amplification. Pull (fan-out-on-read) cost = reads/user/day × avg-followed, with avg-followed ≈ avg-followers for a roughly symmetric graph — the read amplification (tweet fetches per reader). Push wins when reads dominate (a read is then O(1)); pull wins when writes dominate. The celebrity write amplification = 1 post × threshold T followers = the timeline-write thunderclap a single celebrity post would create if pushed — which is why the hybrid stops pushing above T.
Push cost (timeline writes/day)
Pull cost (tweet fetches/day)
Push : Pull ratio
Celebrity write amplification @ T

Interview prompts you should be ready for

  1. 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.)
  2. 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.)
  3. 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.)
  4. 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.)
  5. 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.)
  6. 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.)
  7. 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.)
  8. 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.)
Takeaway
A system-design interview is a derivation under a clock, not a recall quiz. Restate and narrow, then let the numbers — here, 100:1 reads and a 700K/s fan-out with a celebrity tail — pick the architecture. The whole track converges on one habit: reach for each pattern on purpose (cache and replicas for the read path, partition and async fan-out for the write path, the hybrid for the hot key, consensus only off the hot path, eventual consistency spent where it's free, degradation designed for every failure) and defend every trade-off out loud. Optimize what the workload demands, name what you gave up, and attack your own design before the interviewer does. That is the staff-engineer signal.