Part 5 · The capstone · visual edition

Applied Systems Design from first principles

~40 min · 9 designs on 1 method · worked architectures + interview questions · interactive recall

Published

Applied systems designConsistent hashingBack of envelopeMethodEstimation

Your bar: walk into any blank-page design — interview or real review — and never freeze. One repeatable method drives every system in this book, so each design becomes filling in boxes, not improvising. Grounded in Alex Xu’s System Design Interview 1 and Kleppmann’s DDIA. 2

One sentence holds the whole method. Every design just runs it in order:

Clarify functional + non-functional

Estimate QPS, storage, bandwidth

API + data model for the hot query

Draw the boxes & arrows

Deep-dive the bottleneck & name the trade-off

THE METHOD — run it in order, out loud, every time 1 Clarify funct + non-funct 2 Estimate QPS · storage 3 API + data the hot query 4 Draw it boxes & arrows 5 Deep-dive bottleneck the system changes · the method does not · skipping a step is the #1 failure mode levers you reuse on every box: cache · replicate · partition · queue · tune consistency
Memorise this banner. The next eight designs are this method applied to a new requirement set.
1

The design method — never start by drawing boxes

Designing a system is not improvisation. It is a five-step gate you run in order.

The design method, and the building blocks it assembles. Left: five numbered steps as a top-to-bottom flow. Right: a generic web-scale architecture wiring the universal blocks the method draws.

The method (left) and the canonical web-scale shape it draws (right): client → CDN → load balancer → stateless app servers → cache → DB, with a queue + workers for deferred writes and an object store for blobs.

Non-functional dimensionThe question you askWhere the answer lives
ScaleHow many users, QPS, GB/day?Lesson 2 (estimation)
Latencyp99 read/write budget in ms?Storage + caching
AvailabilityTolerate a region loss?Replication, quorums
ConsistencyRead-your-writes, or eventual OK?Consistency models

The hinge question “Is a 200 ms-stale read OK?” Yes → caching + async replication unlock. No → you’ve committed to a quorum or single-leader path, and its cost.

Keep the app tier stateless Push state down into cache + DB; then any replica can die and you scale by adding identical ones behind the LB. 5

Six universal blocks LB · cache · DB · message queue · CDN · object store. You wire these same six for the next eight lessons.

There is no “best” Only an architecture matched to the requirements you clarified in step 1. State the trade-off you chose.

✗ “Start by sketching the architecture”
✓ Start by understanding what + at what scale, then commit to a shape

🧭 Memory rule: Clarify → Estimate → API + data → Draw → Deep-dive. The estimate, not the features, decides whether you shard from day one.

Memory check
  • Five steps in order? → clarify, estimate, API+data, draw, deep-dive
  • Which step forces partitioning? → estimate (the storage/QPS arithmetic)
  • Why keep the app tier stateless? → scale by adding replicas, survive instance loss
2

Back-of-the-envelope estimation — the bridge to architecture

Two minutes of arithmetic decides “one box” vs “a thousand.” Correct to within 10× is enough.

THE LATENCY LADDER — four jumps of ~100× L1 cache · 1 ns RAM · 100 ns  (100×) SSD random read · 100 µs  (1000×) DC round-trip · 0.5 ms disk seek · 10 ms cross-region · 100 ms  (physics) RAM is ~200,000× faster than a cross-region hop → this is why every design has a cache
The takeaway is the gaps, not the digits. Memorise the four ~100× jumps; a cache buys a 1000× leap back up the ladder.3

🧮 The QPS formula: avg QPS = (DAU × actions/day) ÷ 86,400, then peak ≈ avg × 2–3. There are 86,400 seconds in a day — that’s the denominator of every estimate.

Back-of-the-envelope reference card. Left: the latency ladder from L1 cache to cross-region round-trip with bars that grow by orders of magnitude. Right: a worked QPS-and-storage estimate.

Worked example: 100M DAU × 10 reads = 1B/day ÷ 86,400 ≈ 12K avg QPS, ×3 ≈ 35K peak. 1B writes/day × 1 KB × 3 replicas ≈ 1 PB/year → unambiguously a sharded fleet.

The anchor number A record is ~1 KB. A tweet, an order, a Kafka event — call it 1 KB unless you know better. Billions of records → terabytes by inspection.

One multiply per resource QPS, storage, bandwidth, memory each get one multiplication. Whichever blows past a single machine is your deep-dive.

Estimate reads + writes separately 100:1 read-heavy screams cache + replicas; write-heavy screams LSM-trees + a log-first ingest path.

Tail, not average, is the SLO p99 can be 10× the mean under load; at 100-way fan-out a 1-in-100 slow call hits nearly every request. 3

✗ “I’ll size the database, then sanity-check the math later”

✓ The estimate IS the design constraint — it makes the rest inevitable, not arbitrary

Memory check
  • 50M DAU × 20 writes, peak ×3 = ? → 1B/day ÷ 86,400 ≈ 12K avg → ~35K peak
  • Biggest gap on the ladder? → RAM vs cross-region, ~200,000×
  • Why is a 2× error fine? → decisions differ by 1000×, so 2× never flips the answer
3

URL shortener — the canonical read-amplifier

One write is read thousands of times. Optimise the read path first; the write path can be almost naive.

Two-path URL-shortener architecture, write on top and the hot read path below. A counter feeds base62 encoding into a key-value store on writes; redirects are served from a CDN and an in-memory cache, hitting the store only on a miss.

Estimate: 100M URLs/day → ~1.2K wps, ~116K rps (100:1), ~180 TB over 10 years. Two conclusions: reads need a cache + CDN; storage forces partitioning.

Generating the short code — the design’s centre of gravity

ConcernHash of URLCounter + base62
CollisionsPossible; needs retryNone by construction
Same URL → same codeFreeNeeds a lookup table
PredictabilityUnpredictableSequential — guessable
HotspotStatelessCounter is a write hotspot

base62 length math 62⁷ ≈ 3.5 trillion codes — ten years of 100M/day with room. 62⁶ ≈ 56B would run out; 7 chars is the sweet spot.

Break the counter hotspot Hand each app server a range of IDs (a block of 1,000) so it increments locally and coordinates once per block.

Data model = pure KV code (PK) → longUrl, createdAt. Single-key point lookups, no joins → a KV engine beats a relational schema you never query relationally.

Read chain sheds load CDN → cache → read replica → leader. Immutable mappings mean cache invalidation is nearly free — exploit long TTLs.

✗ “Use a 301 redirect — it’s the permanent one”

✓ 301 is browser-cached → you lose click analytics. Pick 302 when tracking is a feature

🔗 Memory rule: A URL shortener is a read-amplifier — counter + base62 gives collision-free codes; the 301-vs-302 call is “save QPS” vs “keep your data.”

Memory check
  • Why read-heavy? → ~100:1 redirects-to-creations
  • Counter+base62's win? → unique codes, no collision retry
  • 301 vs 302? → 301 cached (saves QPS, loses analytics); 302 hits server every time
4

Rate limiter — atomicity on the hot path

The smallest system in the book, and the one that forces an atomic counter on every single request.

Left: the token-bucket algorithm. Right: a shared atomic counter in Redis. Token bucket allows a request when a token is present and returns 429 when the bucket is empty; the distributed counter shows many app servers issuing a single atomic INCR against one Redis key.

Token bucket allows a request when a token is present, returns 429 when empty. Many gateway replicas issue one atomic INCR against a single Redis key so the limit stays global.

AlgorithmMemory / clientAccuracyNotes
Fixed window1 integerLow (2L at edges)Cheapest
Sliding-window counter2 integersHigh (~0.003% err)Best general default
Sliding-window logO(L) timestampsExactCostly at high L
Token bucket2 numbersHigh, allows burstsProduction default

The lost-update trap Naive GET→+1→SET lets two replicas overwrite each other. Redis INCR is atomic by construction → no lost update.

Multi-step? Use a Lua script Refill + decrement + TTL wrapped in EVAL runs as one atomic unit — a transaction collapsed to one instruction.

Where it lives Standard answer: the API gateway — one chokepoint sees all traffic before fan-out. Add per-service limits for expensive endpoints.

Fail-open vs fail-closed If shared Redis is unreachable, usually fail-open (availability over precision) with a per-replica local fallback.

✗ “Local in-memory counters are fine across replicas”

✓ N replicas fragment the count → effective limit becomes N × L; you need one shared counter

🪣 Memory rule: Token bucket allows bursts up to B at average rate r; correctness across replicas needs an atomic shared counter (Redis INCR / a Lua script).

Memory check
  • Why does INCR beat read-modify-write? → atomic single step, no interleaved lost update
  • Fixed-window flaw? → 2L straddling the boundary
  • Token bucket vs leaky bucket? → token allows bursts; leaky forces a smooth outflow
5

News feed — when do you pay the fan-out cost?

A many-to-many aggregation under a read-latency budget. The one decision: aggregate at write time or read time.

Two fan-out strategies for the same new post. Left: fan-out on write copies the post into every follower's precomputed feed (cheap reads, expensive writes); right: fan-out on read merges followees' recent posts at request time (cheap writes, expensive reads); a hybrid uses push for normal users and pull for celebrities.

Push (left): copy a post into every follower’s precomputed feed → cheap reads, expensive writes. Pull (right): merge followees’ posts at read time → cheap writes, expensive reads.

Fan-out on write (push)Fan-out on read (pull)
Cost of a posthigh — write to every followerlow — one insert
Cost of a feed readlow — read precomputed listhigh — merge at read time
Wasted workfeeds for inactive usersnone
Freshnessa few seconds (fan-out lag)always current

Push wins by default Reads dominate (~5:1), so precomputing makes the common op a cheap cache lookup. The cost moves to write time.

The celebrity problem One post by a millions-follower account = a fan-out explosion (hot-key / skewed partition) that floods the write path.

The hybrid real systems run Normal users → push. Celebrities → pull (skip fan-out; merge their recent posts in at read time). You pay pull only where push would explode.

Store IDs, not bodies Feed = per-user list of (postId, authorId, createdAt) in a Redis sorted set, capped ~1,000. A viral post is stored once, referenced N times.

✗ “Pure push scales to every account”

✓ It breaks on the high-follower account; the hybrid push+pull split is the resolution

📰 Memory rule: A feed is many-to-many aggregation under a latency budget — push for the masses, pull for celebrities, and run fan-out as an async outbox/log job.

Memory check
  • Why push as default? → reads dominate; precompute makes them cheap
  • What breaks pure push? → a millions-follower account posting (fan-out explosion)
  • Why store IDs not bodies? → store viral post once, reference N times
6

Distributed cache — trade freshness for latency & load

Every cache decision is a bet on how stale a copy you can tolerate in exchange for not touching the DB.

Cache-aside read flow and the consistent-hashing ring. The left panel numbers the miss path that populates the cache; the right shows keys owned by the next node clockwise and how adding a node remaps only one arc.

Cache-aside (left): on a miss, read the DB, populate, return. Consistent hashing (right): a key is owned by the next node clockwise; adding a node remaps only one arc (~K/N keys), not all of them.

StrategyConsistencyWrite latencyFailure risk
Cache-asideEventual; stale until invalidatedDB onlyStale reads on a race
Write-throughStrong (cache tracks DB)Cache + DBWasted writes
Write-backWeak; cache leads DBCache onlyData loss on crash

The load win 100K rps at 95% hit ratio leaves the DB serving 5K rps — a 20× reduction in the hardest thing to scale.

Why mod-N fails Change N (a deploy, a death) and almost every key remaps → the whole cache cold-starts and hammers the DB. Consistent hashing + virtual nodes fix it.

Cache stampede A hot key expires → every concurrent request misses at once. Fix with request coalescing (single-flight) + jittered/staggered TTLs.

Invalidation is the hard 20% A cache is derived data with no idea when its source changed. TTL is blunt; explicit invalidation is fragile; CDC from the DB’s change stream is the robust answer.

✗ “Write-back is just a faster write-through”

✓ Write-back acks from cache memory → a crash in the flush window loses acknowledged data

🗄️ Memory rule: Cache-aside is the workhorse; consistent hashing keeps reshards cheap; the two things that page you are stampede and invalidation.

Memory check
  • 80K rps at 96% hit → DB rps? → ~3,200 (4% miss)
  • Consistent hashing's win? → a membership change remaps only ~K/N keys
  • Two stampede fixes? → request coalescing + staggered/jittered TTLs
7

Message queue — the partitioned, replicated log

Decouple producers from consumers. The core choice: a log that doesn’t delete on read.

A Kafka-like message queue: producers append to a partitioned topic, each partition replicated leader-to-followers, consumed by a consumer group tracking offsets. Read it as Part 4's log plus Part 1's replication.

Producers append to a partitioned topic; each partition is replicated leader→followers; a consumer group tracks offsets. Estimate: 1M msg/s × 1 KB × 3 days × RF 3 ≈ 780 TB → a disk-resident system.

SettingDurabilityLatencySurvives
acks=0nonelowestnothing

acks=1 (leader)

leader’s disklowfollower loss

acks=all (full ISR)

whole ISRhigherleader loss (2 of 3)

Log > delete-on-read Reading doesn’t delete — the consumer remembers its offset. Buys replayability, multiple independent consumers, cheap append-only durability.

Partition = parallelism + ordering Order holds only within a partition; route by hash(key) mod N so all events for one key stay ordered. More partitions = more throughput, weaker global order.

acks=all is R+W>N in disguise Leader + ISR; an acked write survives losing any 2 of 3 brokers. A new leader is elected from the ISR on failure.

Backpressure = lag A slow consumer just lags (tail offset − consumer offset); the buffer is retention itself. Monitor consumer lag — the #1 operational signal.

✗ “acks=all always means 3 copies”

✓ A silently-shrunk ISR gives acks=1 durability; set min.insync.replicas=2 to reject under-replicated writes

📨 Memory rule: A partitioned append-only log + ISR replication; offset-commit timing picks at-most/at-least-once, and exactly-once = delivery + idempotent processing.

Memory check
  • Why can many groups read one topic? → reads don't delete; each group has its own offset
  • Where is ordering guaranteed? → within a partition, for one key
  • Slow consumer in a log broker? → it lags; safe until past the retention window
8

Key-value store — the Dynamo ring

Where distributed systems and storage engines meet in one box: partitioned, replicated, tunably consistent.

A Dynamo-style key-value store on a consistent-hashing ring. The key hashes to a ring position and is replicated on the next three nodes clockwise; a write reaches a W=2 quorum and a read an R=2 quorum, with R+W>N guaranteeing overlap.

A key hashes to a ring position and is replicated on the next N=3 distinct nodes clockwise (its preference list). A W=2 write and an R=2 read overlap because R+W>N. 4

SettingRWBehaviour
Balanced22One node down tolerated; strong-ish reads
Fast write13Cheap reads, every write hits all replicas
Always writeable31

put survives 2 dead nodes; reads reconcile

R + W > N The W nodes that took the latest write and the R nodes you read must share ≥1 node → the read sees the most recent acknowledged write.

Virtual nodes Each machine claims many small ring positions → load smooths across heterogeneous boxes; a dead node’s keys spread across many successors, not one.

Concurrent writes → vector clocks Track causality, not wall-clock. If neither clock descends from the other, the writes are concurrent — return both siblings, let the app merge (the cart unions items).

LSM per node + gossip Write-heavy → LSM-tree (sequential appends), not a B-tree. Merkle-tree anti-entropy repairs drift; gossip spreads membership in O(log N) with no coordinator.

✗ “Last-write-wins by timestamp is safe”

✓ Clocks drift (no global clock) → LWW silently drops an update; vector clocks detect the conflict

💍 Memory rule: The ring places keys + nodes; the preference list replicates; R+W>N tunes consistency; vector clocks catch conflicts; LSM persists each node.

Memory check
  • R,W for one-down + fresh reads (N=3)? → R=2, W=2
  • What do virtual nodes solve? → load balance + spread a failed node's keys
  • Why LSM not B-tree here? → write-heavy; sequential appends beat random in-place writes
9

Putting it together — the universal checklist & five levers

Every design reduces to: which lever, on which box, and what does it cost?

The universal systems-design checklist as a one-page method. Seven numbered steps flow top to bottom; a recurring-levers palette runs down the right edge so any prompt reuses the same toolkit.

The checklist is a gate, not a menu: Clarify → Estimate → API → Data model → High-level design → Deep-dive the bottleneck → State the trade-offs. The five-lever palette runs down the right.

LeverWhat it buysReach for it when…
CacheRead latency, fewer DB hitsread path is hot
ReplicateRead throughput, availabilitysurvive a node loss / scale reads
Partition / shardWrite throughput, storage past one nodewrites or data exceed one box
Queue (async)Decouple, absorb spikessurvive a traffic spike
Tune consistencyTrade freshness for speed/availabilitystaleness is tolerable

Senior signal Not picking the “right” answer — naming the alternative you rejected and the condition that would flip the call (“…but if this were account balances, I’d want sync replication”).

Immutable data = free caching Pastebin’s pastes never change, so the hard half of caching (invalidation) simply disappears. Spot immutability and exploit it.

Most “scale” is read-scale Cache + read replicas solve a huge fraction of prompts. Write-scale (sharding, outbox, Kafka) is the harder, rarer case.

The single-box baseline Before sharding, ask whether one big NVMe node + a read replica would do. Many “web-scale” problems fit on one box.

✗ “The pipeline delivers each event exactly once”

✓ The wire gives at-least-once; effectively-once = at-least-once + an idempotency key

✅ Memory rule: Faster reads → cache or replicate. Faster writes → partition or queue. Survive a node → replicate. Survive a spike → queue. Name the lever and its cost.

Memory check
  • Why clarify before estimate? → scope fixes the read:write ratio + object sizes the estimate needs
  • Which lever raises write throughput past one node? → partition / shard
  • Critique "exactly-once"? → wire is at-least-once; effective-once needs idempotency keys

Retrieval practice — mix the designs

Reading builds fluency (feels familiar). Recall builds storage (lasts). Answer from memory — instant feedback.

Q1. In the five-step method, which step most directly decides whether you shard from day one?

Q2. Two gateway replicas each GET a counter (5), add 1, and SET 6. What went wrong?

Q3. What specific input breaks pure fan-out on write?

Q4. Why does consistent hashing beat hash(key) mod N for a distributed cache?

Q5. With a log-based broker, what happens when a consumer falls far behind the producer?

Q6. With N=3, which R/W tolerates one node down while a read still sees the latest write?

Reconstruct the method from a blank page

Write the seven checklist steps in order, then the five levers, with nothing on screen. Reveal and compare — gaps are your next drill.


Steps: 1 Clarify (functional + non-functional) · 2 Estimate (QPS, storage, bandwidth) · 3 API · 4 Data model (the hot query) · 5 High-level design (boxes & arrows) · 6 Deep-dive the bottleneck · 7 State the trade-offs.   Levers: cache · replicate · partition/shard · queue (async) · tune consistency.   Reductions: faster reads → cache/replicate · faster writes → partition/queue · survive a node → replicate · survive a spike → queue.

I’m your teacher — ask me anything. Say “grill me on these designs” for mixed no-clue questions, or drop a fresh prompt (“design a chat backend,” “design Dropbox”) and we’ll run the seven-step checklist on it together. Want the next track — spaced, interleaved design drills? Just ask.

Interview — pick your bar

Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).

Walk me through the design method you run on any blank-page prompt.
Hit these points: clarify functional + the four non-functionals (scale, latency, availability, consistency) → estimate QPS, storage, bandwidth → API for the hot operation → data model for the hot query → draw the high-level boxes → deep-dive the one bottleneck and name its trade-off → the system changes but the method does not; skipping a step (usually estimation) is the #1 failure mode → "I clarify before estimating because the read:write ratio falls out of scope."
100M DAU at 10 reads/day each — size the read QPS and storage out loud.
Hit these points: 100M × 10 = 1B reads/day → ÷ 86,400 s ≈ 12K avg QPS → ×2–3 for peak ≈ 35K peak QPS (numbers illustrative) → if those were 1 KB writes: 1B × 1 KB = 1 TB/day, ×3 replicas ≈ 1 PB/year → conclusion each number drives: 35K QPS is past one box → cache + replicas; 1 PB forces a sharded fleet → estimate reads and writes separately because the ratio picks the architecture.
What goes in the API and data model for a URL shortener, and why a KV store?
Hit these points: two endpoints — POST /urls {longUrl} → short code, and GET /{code} → 301/302 redirect → data model is pure key-value: code (PK) → longUrl, createdAt → access is single-key point lookups, no joins or range scans → so a KV engine beats a relational schema you'd never query relationally → the mapping is immutable, so cache invalidation is nearly free and TTLs can be long.
Name the five reusable levers and what each one buys you.
Hit these points: cache → faster reads + DB load shed (trade: staleness) → replicate → survive node loss + scale reads (trade: replication lag / write cost) → partition / shard → more storage + write throughput past one box (trade: cross-shard queries, rebalancing) → queue → absorb spikes + decouple producers from consumers (trade: async, eventual) → tune consistency → lower latency / higher availability by accepting staleness (trade: read-your-writes is no longer free) → every design is "which lever, on which box, at what cost."
A teammate wants to size the database first and check the math later. Push back.
Hit these points: the estimate IS the design constraint, not a sanity check bolted on after → the QPS and storage arithmetic is what decides single box vs replicas vs a sharded fleet → design decisions differ by ~1000× (one machine vs a thousand), so being off by 2× never flips the answer — that's why two minutes of back-of-envelope is enough → without it you've committed to a shape arbitrarily and may discover at build time that storage alone forces sharding you didn't plan for → estimate reads and writes separately: 100:1 read-heavy screams cache + replicas, write-heavy screams LSM-trees and a log-first ingest path.
A rate limiter shares a Redis counter across N gateway replicas. What's the trap and the fix?
Hit these points: the lost-update race — naive GET → +1 → SET interleaves across replicas and silently drops increments, so the effective limit drifts above the target → fix: atomic INCR, which is a single uninterruptible step → for multi-step token-bucket logic (refill + decrement + TTL) wrap it in a Lua EVAL so it runs as one atomic unit → don't use per-replica local counters: N replicas fragment the count to an effective N×L → decide fail-open vs fail-closed when Redis is unreachable (usually fail-open: availability over precision, with a local fallback bucket).
Your cache fronts the DB at a 95% hit ratio. Which two failure modes will page you?
Hit these points: first quantify the win — 100K rps at 95% hit leaves the DB serving ~5K rps, a 20× reduction → (1) cache stampede: a hot key expires and every concurrent request misses at once, stampeding the DB; fix with request coalescing (single-flight) + jittered/staggered TTLs + probabilistic early refresh → (2) invalidation: a cache is derived data with no idea when its source changed; TTL is blunt, explicit invalidation races, CDC from the DB change stream is the robust answer → bonus: a cold-cluster restart is a self-inflicted stampede across every key at once.
When does fan-out-on-write break for a news feed, and how do you resolve it?
Hit these points: push (fan-out on write) is the default because reads dominate (~5:1), so a feed read becomes a cheap precomputed cache lookup → it breaks on the celebrity: one post by a millions-follower account = millions of feed writes, a fan-out explosion that floods the write path and is a hot, skewed partition → resolution is the hybrid: push for normal accounts, pull (merge their recent posts at read time) for accounts above a follower threshold → store post IDs not bodies (a viral post is stored once, referenced N times) in a capped Redis sorted set → run fan-out as an async, idempotent outbox/log job so a spike degrades freshness, not availability.
Make the case that the estimate, not the feature list, should drive the architecture — then steelman the opposite.
For: decisions span ~1000× (one box vs a fleet) and fall directly out of the QPS/storage/bandwidth arithmetic; features tell you what to build, numbers tell you what shape survives; clarifying then estimating makes the rest of the design near-inevitable rather than improvised. Steelman against: early-stage products with unknown load shouldn't shard from day one — over-architecting for hypothetical scale is real cost and operational drag, and premature partitioning can be harder to undo than to add. Synthesis: let the estimate set the shape (stateless app tier, clean partition key chosen even if you run one shard), but defer the cost (actual fleet size) until load is real; design so scaling is "add replicas / split shards," never "re-architect."
You're handed a system that's slow at p99 but fine at the mean. How do you reason about it as the senior in the room?
Hit these points: the SLO is the tail, not the average — p99 can be 10× the mean under load, and at high fan-out a 1-in-100 slow call hits nearly every request, so a "fine average" hides a broken experience → attribute the tail before tuning: is it GC pauses, a cold cache, lock/queue contention, a hot/skewed partition, or a slow downstream dependency? → instrument with percentile histograms and per-dependency latency, not just means → common levers map to causes: stampede → coalescing + jittered TTLs; hot key → better partition key / replication of the hot item; tail amplification from fan-out → hedged requests or reducing fan-out width → name the trade-off of each fix and verify with a before/after percentile, not a vibe.
An engineer proposes adding a cache to "make everything faster." Make the principal-level call.
Hit these points: a cache is not free speed — it's a bet that you can tolerate staleness in exchange for not touching the DB, and it adds the two hardest operational problems in the book (invalidation and stampede) → first ask whether the workload is actually read-heavy and re-readable; caching a write-heavy or low-hit-ratio path adds complexity for little load shed → decide the consistency need: is a 200 ms-stale read OK? If not, you've committed to write-through or a quorum path, which has its own cost → choose the strategy deliberately (cache-aside workhorse vs write-through vs write-back, where write-back can lose acknowledged data on crash) → require a hit-ratio target and an invalidation story (TTL vs CDC) up front → the principal move: cache the specific hot read path with a measured hit-ratio and a defined staleness budget, not "everything."
Design-round framework — drive any blank-page system prompt through these, out loud:
  1. Clarify — functional requirements + the four non-functionals (scale, latency, availability, consistency); state assumptions.
  2. Estimate — QPS (avg & peak), storage/year, bandwidth, memory; read:write ratio.
  3. API — the hot operations and their shapes; pick REST/RPC and idempotency where it matters.
  4. Data model — the hot query first; choose KV / relational / wide-column / log to match the access pattern.
  5. High-level design — wire the universal blocks: client → CDN → LB → stateless app → cache → DB, with a queue + workers and object store as needed.
  6. Scale the bottleneck — vertical first, then horizontal: read replicas, sharding (consistent hashing), caching, async via queue.
  7. Deep-dive & trade-offs — name the one bottleneck with a number, the lever, its cost, the alternative you rejected, and the failure modes.
Design a news feed for 300M users with a sub-200 ms feed-read budget.
A strong answer covers: clarify — home timeline of followees' posts, ranked or reverse-chron, read-your-own-writes for the author, eventual for others → estimate — reads dominate (~5:1), so the read path is the SLO; size feed-read QPS and fan-out write amplification → data model — feed = per-user list of (postId, authorId, createdAt) in a capped Redis sorted set; posts stored once in a KV/blob store → core decision: aggregate at write time (push) or read time (pull) → default push so reads are a cheap precomputed lookup; the celebrity breaks pure push (fan-out explosion / hot partition), so go hybrid: pull for high-follower accounts, merged in at read time → run fan-out as an async idempotent outbox/log job so spikes degrade freshness not availability → cache the hot feeds; CDN media → failure modes: fan-out lag, hot partitions, the inactive-user wasted-work problem → trade-off: push pays at write time and wastes work on inactive users; pull pays at read time; the hybrid pays pull only where push would explode.
Design a distributed key-value store that stays writeable under node and network failure.
A strong answer covers: clarify — durability, target availability, and the consistency bar (do we accept eventual + conflict resolution?) → placement: a consistent-hashing ring with virtual nodes so a membership change moves only ~K/N keys and load stays even → replication: each key to the next N=3 distinct physical nodes clockwise (the preference list), skipping vnodes of the same host → leaderless, tunable quorums R + W > N for strong-ish reads; W=1 for always-writeable (Dynamo's shopping cart) at the cost of more reconciliation → concurrent writes → vector clocks (or version vectors) return siblings to merge, not blind last-write-wins → per-node storage: an LSM-tree for write-heavy load → anti-entropy: Merkle-tree comparison + read-repair + gossip-based membership and failure detection → failure modes: hinted handoff for a briefly-down node, permanent-loss re-replication, and the partition behavior → trade-off: this is an AP design (CAP) — it chooses availability and accepts temporary inconsistency you resolve at read time; if the use case needs strong consistency, you'd move to single-leader-per-shard with synchronous replication instead.

★ Cheat sheet — prompt → lever

The method: Clarify → Estimate → API → Data model → Draw → Deep-dive → State the trade-off. The five levers: cache · replicate · partition · queue · tune consistency. Every prompt = which lever, on which box, at what cost.

Reduction table

You need…Reach for
Faster readsCache (hot set) or read replicas
Faster writes / more storagePartition / shard (consistent hashing)
Survive a node lossReplicate (leader/follower or quorum)
Survive a traffic spikeQueue (async, absorb the burst)
Lower latency, can tolerate stalenessTune consistency (eventual / R+W)
Read-amplifier (shortener, paste)CDN → cache → replica; immutable = free invalidation
Many-to-many feedPush for masses, pull for celebrities (hybrid)
Decouple services + replayPartitioned log; offset = consumer state

Three rules for the design round

① Estimate before you draw — the math, not the features, sets the architecture. ② Name the bottleneck with a number, then a specific lever and its cost. ③ Senior signal = naming the alternative you rejected and what would flip the call.

📄 Full printable version →

★ Review guides & what to read next

5-min (weekly) Don’t re-read — recall. Say the seven steps + five levers; redraw the web-scale shape (CDN→LB→app→cache→DB→queue); pick one design and re-derive its bottleneck from memory.

30-min (when stalled) Blank-page a fresh prompt (chat backend, Dropbox, Twitter search) running the full checklist out loud; for each box, name the lever and its cost, then the alternative you rejected and why.

Read next, in order:

Alex Xu — System Design Interview vol 1 & 2

(the worked designs) · ②

Kleppmann — DDIA

(the theory underneath) · ③ Dynamo paper (Lesson 8) · ④ Kafka design docs (Lesson 7). Latency numbers (Jeff Dean) ground every estimate.

📖 Primary sources: Alex Xu’s System Design Interview and Kleppmann’s DDIA — the two references behind every design here.

Sources
1. Alex Xu, System Design Interview (vol 1 & 2). The four/five-step framework, estimation, and the worked designs (shortener, rate limiter, feed, queue, KV store).
2. Martin Kleppmann, Designing Data-Intensive Applications (DDIA). Reliability/scalability/maintainability (ch.1); indexes (ch.3); replication & quorums (ch.5); partitioning (ch.6); derived data & log brokers (ch.11).
3. Jeff Dean, Latency Numbers Every Programmer Should Know; Dean & Barroso, The Tail at Scale (CACM 2013). The estimation foundation and tail-latency framing.
4. DeCandia et al., Dynamo: Amazon's Highly Available Key-value Store (SOSP 2007). The ring, preference list, quorums, vector clocks, Merkle anti-entropy, gossip.
5. Brendan Burns, Designing Distributed Systems. The replicated-load-balanced-stateless pattern and reusable distributed-systems patterns.
Was this lesson helpful? Thanks — noted.

These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.