Complete offline study guide · senior software engineer

System Design

Everything needed for a senior design round, in one self-contained document: how the round is scored, how to draw the board, the distributed-systems fundamentals underneath it, the building blocks and what each one costs, and twelve canonical prompts worked end to end.

Study window 7 days · ~2 hrs/day Scope generic, company-agnostic Offline no external resources Diagrams 12, all hand-drawable

Part 0

The seven-day plan

Reading this document twice is worth less than reading it once and doing eight design reps out loud. The plan below is built around that ratio: roughly one-third reading, two-thirds talking.

Day 1
2 hr
Frame and drawRead Parts 1 and 2 end to end. Then draw the three-box spine fifty times until it takes under forty seconds without thinking. Do the rate limiter (design 1) out loud, untimed, on paper.Goal: the blank canvas stops being frightening.
Day 2
2 hr
Fundamentals, first halfPart 3 through Replication. Do the arithmetic drills until QPS-from-DAU and storage-with-replication are automatic. Then design 2 (URL shortener), untimed.Goal: never again say "it'll be big."
Day 3
2 hr
Fundamentals, second halfPart 3 from Partitioning to the end — isolation levels, sagas, consensus, clocks. These are the follow-up questions, not the opening ones. Then design 3 (news feed), timed to 45 minutes.Goal: survive the second-order questions.
Day 4
2 hr
Blocks and costsPart 4. For each block, cover the page and recite "reach for it when ___, the cost is ___." Then designs 4 and 5 (chat, notifications), timed.Goal: every technology you name arrives with a price tag.
Day 5
2 hr
Patterns and operationsPart 5. Pay special attention to hosting, observability and security — they are the sections most candidates have never rehearsed. Then designs 6 and 7 (scheduler, metrics), timed.Goal: answer "how would you deploy and operate this?"
Day 6
2.5 hr
VolumeDesigns 8 through 12, timed to 40 minutes each, no reading between them. This will be tiring and that is the point — the round is at the end of a long day too.Goal: stamina and pattern recognition.
Day 7
2 hr
Cold prompts and reviewTwo prompts you have not seen (invent them or take them from a friend), recorded, self-graded against the checklist in Part 7. Then read the cheat sheet and the drills. Nothing new.Goal: walk in with a rehearsed opening and a rehearsed closing.
If you only have three days Day 1, Day 4, and Day 6 in that order. Skip Part 3's second half and Part 6's designs 9–12. The framing, the trade-off vocabulary, and volume of reps are what move the score; the deep fundamentals mostly protect you against follow-ups.

How to do a rep properly. Stand up. Set a timer for 45 minutes. Say every word out loud, including the boring ones. Draw on real paper or a real whiteboard, not in your head. When the timer stops, stop mid-sentence and write down where you were — running out of clock at the data model means your requirements phase is too slow, running out at the deep dive means your architecture phase is too broad. Then score yourself against the checklist. The self-scoring is not optional; it is where the improvement comes from.

Part 1

The interview itself

Before any technical content: what this round is, what it measures, and the twelve minutes of behaviour that decide most of the outcome.

What is actually graded

The prompt will be one underspecified sentence — “design a system that lets users share photos,” “design something that runs scheduled data pipelines.” That vagueness is deliberate. The interviewer is not withholding a spec; there isn't one, and watching you construct it is the first graded moment.

Interviewers are typically scoring four axes, roughly in this weight order:

  1. Scoping and requirements. Did you convert an ambiguous prompt into a bounded problem with numbers attached, and did you say what you were not building?
  2. Trade-off reasoning. Not “I'd use a queue” but “I'd use a queue, and what I'm giving up is end-to-end latency and global ordering.” This is the axis that most separates senior from mid.
  3. Technical depth. One component taken to the bottom — its data structures, its failure modes, its bottleneck — beats fifteen boxes each explained for ten seconds.
  4. Collaboration. They were told to act as your design partner. Candidates who check in, take hints, and revise score higher than candidates who present a finished answer.

Note what is not on that list: knowing product names, producing a complete system, being certain. In 45 minutes you produce a credible skeleton plus one component in real depth, and you are allowed — encouraged — to change your mind when the numbers say to.

What it is not

The clock

Memorize this. It is your safety net when the prompt is vague and your mind is blank.

MinPhaseThe thing you must actually do
0–6RequirementsAsk. Write answers in the corner of the board. Say what is out of scope.
6–10ScaleArithmetic out loud. Land on “so roughly N QPS and M TB/year.”
10–13InterfaceThree to five operations, signature level, not implementation.
13–17Data modelEntities, primary key, partition key. Sharding gets decided here.
17–27ArchitectureDraw it. Then trace one write and one read through the whole picture.
27–40Deep divePick the hard component unprompted. Then failure modes.
40–45WrapWhat breaks first at 10×. What you'd ship as v1. Your questions.

If you remember one thing: minutes 0–10 are requirements and numbers, and skipping them is the most common way to fail a round you were qualified for.

The opening, near-verbatim “Let me start by making sure I understand the problem and pinning down scope, then I'll do some quick capacity math, then sketch an architecture and go deep wherever you think is most interesting. Sound good?”

That sentence does three things at once: it buys you permission to spend six minutes not drawing, it tells the interviewer you have a structure, and it invites them to steer. Say it every single time.

The scoping questions

Ask in roughly this order. Write every answer where you can both see it — you will point back at this list four times during the round.

  1. Who are the users and what is the core use case? Internal engineers or external customers? One tenant or many?
  2. What are the top two or three things it must do? Then immediately: “anything I should treat as out of scope?”
  3. How big? Users, requests per second, data volume, growth rate.
  4. Read-heavy or write-heavy, and what ratio? This single answer determines most of the architecture.
  5. Latency target? Interactive (p99 under 200 ms) or batch (minutes are fine)?
  6. Consistency? Must a read see the last write immediately, or is a few seconds of staleness acceptable?
  7. Availability and durability? What actually happens if it's down for five minutes? Is losing data ever acceptable?

Then say the answers back as one sentence: “So: about 50,000 writes per second, reads can be a few seconds stale, availability matters more than strict consistency, and multi-region is out of scope for v1. I'll design against that.” This summary is worth several minutes of design work, because everything after it can be justified by pointing at it.

The non-functional checklist — free points

Name these explicitly in the requirements phase. Most candidates forget most of them.

PropertyThe question to ask yourself
ScalabilityWhat dimension grows — users, data, or fan-out? They have different answers.
AvailabilityWhat's the SLO? 99.9% is 43 min/month of downtime; 99.99% is 4.3.
LatencyAlways discuss p99, not average. Averages hide the failure.
ConsistencyStrong, read-your-writes, or eventual? Per operation, not per system.
DurabilityCan we ever lose an acknowledged write? Usually no — say so.
CostStorage × replication × retention. Mentioning cost reads as seniority.
SecurityAuthN vs authZ, encryption in transit and at rest, tenant isolation.
ObservabilityWhat do you page on? What's the one dashboard?
OperabilityHow do you deploy this without downtime? How do you migrate the schema?

The failure modes, in order of frequency

FailureWhat it looks likeFix
Diving inDrawing boxes in minute twoFive to eight minutes on requirements. Always.
SilenceLong pauses while thinkingNarrate: “I'm weighing X against Y.”
No numbers“It'll be pretty big”Do the arithmetic. Ninety seconds, enormous credibility.
Breadth onlyFifteen boxes, none explainedGo deep on one component unprompted.
Buzzword saladNaming tech without justifying itEvery technology gets “because ___” and “the cost is ___.”
Ignoring the hintSteamrolling past a nudgeA hint is not a suggestion. Follow it immediately.
Defending a mistakeArguing when shown a flaw“You're right, let me revise.” Revision scores; defence doesn't.
Running out of clockNo deep dive, no wrap-upWatch the time. At minute 27, move whether or not you're done.

Part 2

Drawing the board

Bad diagrams are almost never a hand-skill problem — they are a vocabulary and a layout problem, and both are fixable in an afternoon. Every diagram in this document is drawable with a straight edge and a wobbly ellipse.

Six shapes, two arrows

When every component is the same rectangle, the picture carries no information the labels don't already carry, and the interviewer has to read instead of see. Commit to six shapes.

svc Rectangle stateless service Cylinder durable store / truth cache Stadium volatile / droppable Ticked box queue or log Trapezoid router / balancer Dashed pen boundary: region, AZ, trust synchronous — the caller waits, and inherits the failure asynchronous — the caller moves on, someone else retries
The whole vocabulary. Shape carries the property that matters most about a box: can it be scaled by adding copies, can it lose data, does it order things. Arrow style carries who waits.

Two payoffs. The shapes do work your mouth would otherwise have to do — a stadium instead of a rectangle already says “losing this costs latency, not data.” And the moment you draw a cylinder you have committed to answering “what's the partition key?”, which is a question you want to be asked.

One shape to resistThe cloud. A cloud means “something happens here and I have not thought about it.” If you draw a box you cannot name a partition key or a failure mode for, you have drawn a cloud with corners.

The invisible grid

Two laws and three reserved zones, set up in the first ten seconds. Left to right is the request: anything a user waits for flows left→right, no exceptions. Top to bottom is depth: higher is closer to the user, lower is closer to the disk.

REQUIREMENTS · NUMBERS · OUT OF SCOPE Written in minutes 0–10. Never erased. You will point at it four times. 1 · CLIENT 2 · EDGE 3 · COMPUTE 4 · STATE who calls it, how many DNS, CDN, LB, gateway stateless boxes you can add to cache, store, partition key ANNEX Leave it empty. Minute 27 needs room for one box drawn big. ASYNC · OFFLINE Queues, workers, batch jobs, analytics. Nothing down here is on the request path, which is exactly the point — say that out loud when you draw the first dashed arrow.
Zone the board before you draw. The requirements strip and the empty annex are the two zones candidates skip, and they are the two that decide whether minutes 27–40 have anywhere to happen.

Use about the left two-thirds for the main picture. On a shared virtual canvas, never zoom out to fit — the interviewer sees your viewport, and a diagram they have to squint at reads as one you lost control of.

Draw order

The single biggest improvement available: stop drawing the finished picture. Draw four pictures, each a small delta on the last, narrating the delta. The interviewer watches you reason, and you never face a blank canvas with a whole system in your head.

  1. The spine. Three boxes — who calls it, the thing, where state lives — and two arrows. Forty seconds. Always correct.
  2. Label every arrow, then trace one request. Verb and payload on each edge, then walk one write and one read end to end, touching each box. Errors surface here, cheaply.
  3. Widen only where a number forced it. Add the balancer because you computed the QPS; the cache because you computed the read ratio. Say the number as you draw the box.
  4. Annex the deep dive. Ring one component, draw its interior large in the space you reserved. Do not erase the main picture.
1 · THE SPINE — 40 SECONDS, ALWAYS CORRECT client service db 2 · LABEL THE ARROWS, THEN TRACE ONE REQUEST OUT LOUD client service db POST /order · ~2 KB insert, PK = order_id idempotency key here one row, one round trip 3 · WIDEN ONLY WHERE A NUMBER FORCED IT client LB svc svc ×12 — 8K QPS cache 98% of reads db events queue worker olap nothing below the dashed line blocks a user 4 · RING ONE BOX, DRAW ITS INSIDE BIG IN THE ANNEX cache req cache miss single-flight lock · TTL + jitter hot key → shard it the stampede lives here
Four deltas, not one drawing. Stage 3 is where most people start — and starting there is why the picture ends up crowded, unlabeled, and out of room. Note that stage 4 ghosts the main diagram rather than erasing it.
Never eraseErasing destroys the record of your reasoning, which is the artifact being graded. Cross it out, draw the replacement beside it, and say “let me revise that.” A visible correction is a positive signal; a clean board is not.

Labeling

An unlabeled arrow means “these two things are related somehow,” which is worth almost nothing. Labels are where the design lives.

WHAT MOST PEOPLE DRAW svc db “related somehow” zero information WHAT SCORES svc db insert · 2K/s peak sync, 5 ms budget stateless ×12 PK = user_id 16 shards, RF 3 fails → 503, client retries w/ jitter
Same two boxes, two very different candidates. Six short annotations turn a shape into a design. None of them takes more than three seconds to write.

Marker and mouse

Physical whiteboard

Draw boxes about a fist wide — you will need to write inside them later. Print in capitals. Keep one marker in hand and a second colour capped in your pocket for annotations only. Stand to the side of what you drew.

Virtual canvas

Learn exactly four keys: rectangle, line, text, select. Turn snapping on. Don't hunt for a cylinder icon — draw a rectangle, write db in it, and say “pretend that's a cylinder.” Every interviewer accepts this instantly.

If drawing is slow for you

Narrate ahead of your hand: “I'm putting the client here, the service in the middle, storage on the right.” The interviewer follows the words. Speed of hand is not on the rubric; thirty seconds of silence is.

Four bad diagrams

Constellation: no left-right flow → redraw the spine underneath. Inventory: many boxes, no depth → ring one. Mute: unlabeled edges → add verbs. Wall: board full at minute 20 → ghost the left half, work in the margin.

Part 3

Distributed systems fundamentals

The material underneath the boxes. You will rarely be asked to recite any of this, but every follow-up question in the deep-dive phase comes from here, and being fluent is what lets you answer “why?” three times in a row.

Numbers and arithmetic

Two minutes of arithmetic buys more credibility than ten minutes of boxes. Round aggressively — nobody wants precision, they want to see you can do it at all.

OperationTimeWhy it matters
L1 cache reference~1 nsFree
Branch mispredict~3 nsOnly matters in tight loops
Mutex lock/unlock~25 nsCheap until contended, then catastrophic
Main memory reference~100 ns100× slower than L1
Compress 1 KB~2 µsUsually worth it before a network hop
Read 1 MB sequentially from memory~50 µs
SSD random read~100 µs~1000× slower than RAM
Round trip inside a datacenter~0.5 msThe price of one extra service hop
Read 1 MB from SSD~1 ms
Disk seek (spinning)~10 msWhy random disk I/O is the enemy
Read 1 MB from spinning disk~20 ms
Round trip US coast to coast~70 msSpeed of light. Not negotiable.
Round trip cross-Atlantic~150 msWhen a second region stops being optional

Three ratios explain most design decisions: memory is ~1000× faster than SSD; SSD is ~100× faster than spinning-disk random I/O; one cross-continent round trip costs more than thousands of local disk reads. Almost every architecture argument is one of these three ratios in disguise.

Capacity arithmetic

Anchors worth memorizing. One commodity machine: ~10K QPS of simple requests, a few TB of SSD, ~64–256 GB RAM. A single relational instance: comfortable to ~10–50K reads/sec behind a cache, and hurting on writes well before that. 1 KB × 1M rows = 1 GB; 1 KB × 1B rows = 1 TB. A single Kafka-class partition: ~10 MB/s sustained.

Availability arithmetic

SLODowntime / monthDowntime / yearWhat it costs to get there
99% — two nines7.2 hr3.65 daysOne machine, best effort
99.9% — three nines43 min8.8 hrRedundancy + health checks + on-call
99.99% — four nines4.3 min53 minMulti-AZ, automated failover, canary deploys
99.999% — five nines26 sec5.3 minMulti-region active-active. Very expensive.

Two things to say. Dependencies multiply: a service calling five dependencies each at 99.9% has a ceiling of 99.5% unless it can degrade without them — which is the actual argument for circuit breakers and graceful degradation. And most candidates over-promise: proposing five nines for a v1 internal tool is a scoping failure, not ambition.

Forty seconds, out loud — worth ten minutes of boxes “100M daily users posting twice a day is 200M writes a day, over 105 seconds, so about 2,000 writes per second average — call it 10,000 at peak. At 1 KB each that's 200 GB a day, roughly 70 TB a year, times three for replication, so about 200 TB. That's well past one machine, so we're sharding from day one; and at 10K writes per second I want the write path to be append-only.”

The network

Almost every latency budget is dominated by network, and almost every candidate skips straight to the load balancer. Being able to start one hop earlier is cheap credibility.

What a first request actually costs

DNS resolution (0–50 ms, then cached for the TTL — it is not paid per request). TCP handshake, one round trip. TLS handshake, one further round trip on TLS 1.3, two on 1.2 — and zero on resumption. Then the request itself. This is why connection reuse, keep-alive, and connection pools matter so much, and why a “simple” extra service hop is never free.

Protocol choices

ProtocolUse whenCost / caveat
REST / HTTP+JSONPublic APIs, broad compatibility, cacheable GETsVerbose; no streaming; over- and under-fetching
gRPC / protobufInternal service-to-service, high volume, streamingBinary (harder to debug); needs schema management; browser needs a proxy
GraphQLMany clients with different shape needsQuery cost is unbounded unless you limit depth; caching is hard
WebSocketBidirectional, low-latency, long-lived (chat, presence)Stateful connections → sticky routing, and connection count becomes a capacity dimension
Server-Sent EventsServer→client push only (feeds, notifications)One direction; auto-reconnect is built in, which is nice
Long pollingFallback when the above are unavailableHolds a connection per client; wasteful but very compatible

Push versus pull is the underlying question and it comes up constantly. Pull (polling) is simple, stateless, and wastes requests when nothing changed. Push is efficient and forces you to hold connection state and solve delivery for offline clients. The usual real answer is push with a pull-based reconciliation on reconnect — say that.

HTTP versions, in one line each

HTTP/1.1: one request at a time per connection (head-of-line blocking), so browsers open six. HTTP/2: multiplexes many streams over one TCP connection, but a lost packet stalls all of them. HTTP/3 (QUIC over UDP): removes transport-level head-of-line blocking and cuts handshake round trips. You rarely need more than this in a design round.

Consistency models

“Strong or eventual?” is a false binary and treating it as one is a common tell. There is a ladder, and different operations in the same system usually sit on different rungs.

ModelGuaranteeCost
LinearizableEvery read sees the most recent write; the system behaves as if there is one copyCoordination on every operation; latency floor set by consensus round trips; unavailable during partitions
SequentialAll nodes see operations in the same order, not necessarily real-time orderCheaper than linearizable, still needs agreement on order
CausalOperations that causally depend on each other are seen in order; concurrent ones may differNeeds causality tracking (vector clocks); often the sweet spot
Read-your-writesA client always sees its own writesSession pinning or a version token. Cheap, and usually what users actually notice
Monotonic readsYou never see time go backwards across successive readsPin a session to one replica
Consistent prefixYou never see an answer before its questionOrdering within a partition
EventualReplicas converge if writes stopNothing — and it guarantees nothing about when
The move that scores Assign a model per operation, not per system. “Posting a comment needs read-your-writes so the author sees it immediately; the global comment count can be eventually consistent and thirty seconds stale; the payment ledger needs linearizability. Three different answers in one system.”

CAP and PACELC

CAP: during a network partition, you must choose availability or consistency. That is a narrower claim than it is usually given credit for — it says nothing about normal operation. Do not lecture on it.

PACELC is the more useful sibling: if there's a Partition, choose Availability or Consistency; Else, choose Latency or Consistency. The second half is what you actually trade on every request of every day. A design that says “I'm choosing latency over consistency here, and this is what the user might see” is doing real work.

Replication

Replication is copies of the same data. It buys read throughput and survival; it does not buy write throughput. Confusing it with partitioning is one of the most common vocabulary errors in these rounds.

SchemeHow it worksWhat it costs
Leader–follower, asyncWrites to one leader; followers stream the log behind itReplication lag → a user may not read their own write. Failover can lose recent writes.
Leader–follower, syncLeader waits for a follower to acknowledge before committingWrite latency now includes the slowest follower; if it's down, writes stall
Semi-syncWait for one of N followersThe usual practical compromise: no data loss on single-node failure, bounded latency
Multi-leaderWrites accepted in multiple regions, replicated both waysWrite conflicts you must resolve. Last-write-wins silently loses data
Leaderless / quorumN replicas; write to W, read from R. If W + R > N reads see the latest writeTunable per query, but you own conflict resolution and read repair

The lag consequence to volunteer: with async replication a user posts a comment, the read goes to a lagging follower, and their own comment is missing. Fixes: pin that user's reads to the leader for a few seconds after a write; or pass a version token the replica must have caught up to; or read from the leader for anything the user just touched.

Conflict resolution, when writes can happen in two places

Partitioning (sharding)

Partitioning is different data on different machines. It buys write throughput and capacity. You shard when one machine can no longer hold the data or absorb the writes — and not before, because sharding costs you cross-shard queries, cross-shard transactions, and rebalancing forever.

The choice of partition key is the single most consequential decision in most designs. Say that out loud, then think about it out loud.

SchemeGood atBad at
Range (by time, by A–Z)Range scans are cheap and localHot shard — today's partition takes every write
HashEven distribution in expectationRange queries must fan out to every shard; resharding moves everything
Consistent hashingAdding or removing a node moves only ~1/N of keysMore complex; needs virtual nodes to be even at all
Directory / lookup tableTotal flexibility, easy rebalancing, can isolate big tenantsThe directory is now a critical dependency on the hot path
Composite (tenant + hash)Isolates tenants and spreads within themTwo decisions to get right instead of one
The skew observation — say this unprompted “Hashing on user id is even in expectation, but if one enterprise customer is 30% of traffic, that shard is hot no matter what. So I'd want a way to split a single tenant across shards — a composite key of tenant plus a bucket number — or to isolate the largest tenants onto dedicated shards.”

Secondary indexes across shards

An under-discussed topic that reliably impresses. Local (document-partitioned) index: each shard indexes only its own data — writes are cheap and local, but every query by that index must scatter-gather across all shards. Global (term-partitioned) index: the index itself is partitioned by the indexed term — reads hit one shard, but writes now touch two partitions and you need to keep them consistent, usually asynchronously. Name which one you're picking and why.

Rebalancing

Never use hash(key) % N where N is the node count — adding a node remaps almost every key. Use a fixed large number of logical partitions (say 1,024) assigned to physical nodes, so growing the cluster moves whole partitions and nothing gets rehashed. This is the standard answer and it is worth a sentence.

Transactions and isolation

ACID: Atomicity (all or nothing), Consistency (invariants hold — really an application property), Isolation (concurrent transactions don't corrupt each other), Durability (a committed write survives a crash, which in practice means it hit a write-ahead log on stable storage).

Isolation is the one worth knowing properly, because “we'll use a transaction” is not an answer until you say which level.

LevelPreventsStill allows
Read uncommittedNothing muchDirty reads. Essentially never what you want.
Read committedDirty reads and dirty writesNon-repeatable reads: the same row read twice in one transaction differs
Snapshot / repeatable readNon-repeatable reads — the transaction sees a consistent snapshotWrite skew and phantoms: two transactions read the same state and each makes a decision that is only safe if the other didn't
SerializableEverything. Result is equivalent to some serial orderNothing — but you pay in aborts, locks, or throughput

Write skew, the classic example: two doctors are on call; each checks “is at least one other doctor on call?”, sees yes, and both go off call. Snapshot isolation permits this because neither wrote the row the other read. The fixes are serializable isolation, an explicit lock on the thing you're reasoning about (SELECT ... FOR UPDATE), or materializing the conflict into a row you do write.

How isolation is implemented

Distributed transactions and delivery semantics

The moment a business operation spans two services or two shards, atomicity stops being free. There are exactly four workable answers and you should be able to name all four.

  1. Don't. Redesign so the atomic unit lives in one partition. Choosing a partition key that keeps related entities together is the cheapest possible fix and the one to propose first.
  2. Two-phase commit. A coordinator asks everyone to prepare, then to commit. Correct, but it blocks: if the coordinator dies after prepare, participants hold locks indefinitely. Mention that you'd rather avoid it.
  3. Saga. A sequence of local transactions, each with a compensating action to undo it. No global locks, and the system is temporarily inconsistent by design. The realistic answer for cross-service workflows — but compensations are business logic, not a rollback, and “un-charge the card” is not always possible.
  4. Outbox + idempotency. The workhorse. See below.

The dual-write problem and the outbox

You cannot atomically write to your database and publish to a message log — one of them will succeed and the other won't, and then your state and your events disagree. The fix: write the event as a row in an outbox table in the same transaction as the state change, then have a separate relay process read the outbox and publish. Atomicity is restored because there's only one write. This is the single most useful pattern to be able to draw from memory.

Delivery semantics

SemanticReality
At-most-onceFire and forget. Messages can be lost. Fine for metrics samples, never for money.
At-least-onceThe default everywhere. Retries can duplicate. Consumers must be idempotent.
Exactly-onceImpossible end-to-end across a network. What systems actually offer is at-least-once delivery plus deduplication, or transactional read-process-write inside one system's boundary.
The most reusable sentence in system design “This is at-least-once, so the consumer has to be idempotent — I'd dedupe on the event id with a table of processed ids, or make the operation naturally idempotent by keying the write on a client-supplied idempotency key.”

How to make something idempotent, concretely: a client-generated request id stored with a uniqueness constraint; an upsert keyed on a natural id instead of an insert; a conditional write with a version number (compare-and-set); or a state machine that ignores transitions it has already made.

Consensus, leases and time

Consensus is how a set of machines agree on one value despite failures. In an interview you need the shape, not the proof.

Raft in six sentences

Nodes are follower, candidate, or leader. A follower that hears no heartbeat becomes a candidate and requests votes for a new term. A candidate with a majority becomes leader. All writes go to the leader, which appends to its log and replicates; an entry is committed once a majority has it. A majority is required for both election and commit, which is why an odd number of nodes (3, 5) is standard — and why the cluster stops accepting writes rather than splitting when it loses quorum. Reads either go through the leader or require a lease to be safe.

Where to use it: leader election, cluster membership, configuration, distributed locks, small critical metadata. Where not to: the data hot path. Consensus is a coordination bottleneck; you use it to decide who is in charge, not to serve every request.

Leases and fencing tokens — the detail that separates candidates

A lock with a timeout is a lease. The trap: a slow process is indistinguishable from a dead one. A worker takes a lease, pauses for a long garbage collection, its lease expires, the system hands the work to someone else — and then the first worker wakes up and writes, believing it still holds the lock. Now two writers.

The fix is a fencing token: the lock service issues a monotonically increasing number with each lease, the worker includes it in every write, and the downstream store rejects any write carrying a token lower than the highest it has seen. Being able to explain that failure and that fix is a genuinely strong signal.

Clocks

Failure detection

Heartbeats with a timeout are the baseline — the timeout is a guess, and a wrong guess either declares healthy nodes dead or notices real deaths too slowly. Gossip spreads membership information without a central coordinator and scales to large clusters. Phi-accrual detectors output a suspicion level from the observed distribution of heartbeat intervals rather than a binary alive/dead, which adapts to a network that is merely slow. The point to make: you cannot distinguish a dead node from a slow network, so every design must be correct under both interpretations.

Part 4

The building blocks

For each block: what it is, when you reach for it, and what it costs. The third column is the one that scores. Every technology you name in a round owes the interviewer a “because ___” and a “the cost is ___.”

The front door

BlockReach for it whenWhat it costs
DNS / anycastRoute users to the nearest or healthiest regionSlow to change (TTL); not a real failover mechanism on its own
CDNStatic assets, cacheable responses, global users, large payloadsInvalidation is hard; useless for personalized content; another cache to reason about
Load balancerMore than one instance of anythingL4 is fast and TCP-level; L7 can route on path or header but costs CPU and terminates TLS
API gatewayAuth, rate limiting, quotas, routing belong in one placeA shared component on every hot path is a correlated failure and a deploy bottleneck
Reverse proxy / sidecarmTLS, retries, circuit breaking without touching app codeOperational complexity; another hop; a whole control plane to run

Load balancing algorithms, briefly

Round robin ignores that requests differ in cost. Least connections is better when request durations vary widely. Least outstanding requests is the practical default at scale. Consistent hashing when you need the same key to land on the same backend (cache affinity, sticky sessions) — and it is worth saying that stickiness is a form of state you are choosing to accept. Power of two random choices — sample two backends, pick the less loaded — gets most of the benefit of global load awareness with none of the coordination, and mentioning it is a nice touch.

Health checks deserve a sentence: shallow checks (is the process up) miss a broken dependency; deep checks (can I reach my database) can take your whole fleet out of rotation simultaneously when that dependency blips. The usual answer is a shallow check for the balancer plus a separate deep readiness signal.

Rate limiting — know all four algorithms

AlgorithmHowTrade-off
Fixed windowA counter per (key, minute)Trivial. Permits a 2× burst across the boundary — 100 requests at 11:59:59 and 100 more at 12:00:00
Sliding window logStore every timestamp; count those inside the windowExact. Memory grows with request rate
Sliding window counterWeighted blend of the current and previous windowNear-exact, O(1) memory. Usually the right answer
Token bucketTokens refill at rate R up to capacity C; a request costs oneO(1), and it permits controlled bursts — usually what an API actually wants
Leaky bucketRequests queue and drain at a fixed rateSmooths output perfectly; adds queueing latency

Caching

The highest-value block to know deeply, because caching questions have crisp right answers and most candidates only know one strategy.

StrategyHowTrade-off
Cache-aside (lazy)App checks cache; on miss reads the store and populatesSimple and most common. First request is always slow; stale data possible; cache and store can diverge
Read-throughThe cache itself loads on missCleaner app code; the cache becomes a dependency of correctness
Write-throughWrite hits cache and store togetherCache is always fresh; every write is slower
Write-behindWrite to cache, flush to store asynchronouslyVery fast writes; you can lose data on a crash
Refresh-aheadProactively refresh hot keys before expiryHides latency for predictable keys; wasted work for the rest

The four problems to raise before you're asked

Eviction policies: LRU is the default; LFU is better for stable popularity distributions; TTL-only is simplest and wastes memory; W-TinyLFU (admission control plus LFU) is what modern caches use, and mentioning that admission matters — caching an item that is never read twice is pure cost — is a good detail.

Storage engines and datastores

Pick the shape that matches the access pattern

ShapeReach for it whenWhat you give up
RelationalDefault. Joins, transactions, secondary indexes, strong consistencyWrite ceiling of one machine; schema migrations on huge tables are painful
Key-valueO(1) access by a known key at enormous scaleNo joins, no ad-hoc queries, no range scans unless designed in
Wide-columnVery high write throughput, tunable consistency, time-ordered rowsYou design a table per query; you own compaction and repair
DocumentNested objects, flexible or evolving schemaWeaker consistency and join stories; easy to denormalize into a corner
Time-seriesAppend-heavy, always queried by time range, downsampled over ageSingle-purpose; cardinality becomes the failure mode
GraphTraversals many hops deep are the primary queryHarder to shard; a narrower operational ecosystem
Search indexFull text, faceting, relevance rankingIt is an index, never the source of truth. Say this — it is a common trap
Object storeBlobs over ~100 KB, backups, logs, data-lake filesNo partial updates, higher latency, eventual listing consistency

The honest answer to “SQL or NoSQL?” is “it depends on the access pattern and the consistency requirement, not on the scale” — then pick one and defend it. Blanket “NoSQL scales better” is a weak answer; a sharded relational database scales fine, and you keep transactions.

B-tree versus LSM tree — worth one confident sentence

B-treeLSM tree
WritesIn-place update, random I/O, write-ahead log for durabilitySequential append to an in-memory table, flushed to immutable sorted files
ReadsPredictable: one tree traversalMay touch several files; bloom filters skip most of them
StrengthRead-heavy, point lookups, strong transactional supportWrite throughput, better compression, no in-place mutation
CostWrite amplification from page splits; random write I/OCompaction consumes I/O and causes latency spikes; space amplification

“This is write-heavy, so I want an LSM-based store — I'm accepting compaction overhead and read amplification in exchange for sequential writes” is a genuinely senior sentence.

Other storage vocabulary you should have

Write-ahead log: append the intent before mutating, so a crash is recoverable — the basis of durability everywhere. Bloom filter: “is this key definitely absent?” No false negatives, some false positives; used to avoid disk reads in LSM trees. Index: a second data structure that speeds reads and slows every write — say that trade when you add one. Covering index: contains all columns a query needs, so the base table is never touched. Denormalization: trading write complexity and storage for read speed; the standard move when reads dominate by 100:1.

Queues, logs and streams

Reach for asynchrony when you want to decouple producers from consumers, absorb bursts, retry failures without blocking a user, or fan one event out to many consumers. The distinction that matters:

Work queueDurable log
ModelA message is delivered to one consumer and removedAn append-only sequence; consumers track their own offset
ReplayGone once acknowledgedReplayable — reprocess history by rewinding the offset
Fan-outNeeds a topic/exchange per consumerNative: many independent consumer groups read the same log
OrderingUsually best-effortStrict per partition, never global
Best forTask distribution, jobs, RPC-ish workEvent streams, CDC, pipelines, rebuilding derived state

Things to say about any queue

Stream processing vocabulary

Windowing: tumbling (fixed, non-overlapping), sliding (overlapping), session (gap-defined). Event time versus processing time: events arrive late and out of order, so aggregating by arrival time gives wrong answers. Watermarks: a declaration that you believe all events up to time T have arrived, which is what lets a window close. Late data: either drop it, or emit a correction. Checkpointing: periodic state snapshots so a failed operator resumes rather than restarts.

Lambda versus kappa architecture: lambda runs a batch path and a streaming path in parallel and reconciles (accurate, but two implementations of every computation); kappa runs only the stream and replays the log when it needs to recompute. Kappa is usually the better answer now, precisely because the log is replayable.

Probabilistic structures — cheap credibility

Reach for these the moment someone says “now there are a billion elements and you have one gigabyte of RAM.”

StructureAnswersError
Bloom filter“Is this key definitely absent?”No false negatives; tunable false positives. Cannot delete (use a counting variant)
HyperLogLogApproximate distinct count~2% error in a few KB instead of GB. Mergeable across shards
Count-min sketchApproximate frequency — heavy hitters, hot-key detectionOvercounts, never undercounts
t-digest / histogramPercentiles over a streamApproximate — but you cannot average percentiles, so this is the only correct way to aggregate p99 across hosts
Reservoir samplingA uniform sample from a stream of unknown lengthSampling error only

“You can't average percentiles” is a small, extremely reliable credibility win in any monitoring or metrics discussion.

Part 5

Architecture patterns

Eight pictures worth having in muscle memory, plus the operational material — deployment, observability, security — that senior rounds ask about and most candidates have never rehearsed.

End to end: where a request actually goes

Most candidates start their diagram at the load balancer. Starting one hop earlier and narrating every stop to the disk and back is where the latency-budget conversation comes from.

DNS resolver once per TTL, not per request client CDN edge PoP TLS 30 ms (0 if reused) LB 25 ms cross-country wire, unavoidable <1 ms gateway authn, quota 2 ms service 3 ms cache 0.5 ms 98% stop here db miss 5 ms response retraces every hop — the 25 ms is paid twice TOTAL ≈ 65 ms p50, of which ≈55 ms is network you cannot optimize in code. Which is the entire argument for the CDN, for edge caching, and for not being chatty.
One request, every stop, with a budget. Drawing the return path is unusual and lands well — it makes the cost of a chatty client, or an extra synchronous hop, visible rather than theoretical.
The sentence this diagram buys you“Before I optimize anything server-side — the budget here is 65 milliseconds and 55 of it is speed of light. So the first lever isn't a faster database, it's terminating closer to the user and making fewer round trips.”

Two follow-ups to be ready for. “What if the client is in Europe?” The 25 ms becomes 90 ms each way, which is when read replicas or a second region stop being optional. “Where would you put retries?” At the client with jitter and at the gateway — never at every layer, because layered retries multiply into a retry storm the moment the origin gets slow.

Where it actually runs

“How would you deploy this?” is asked in most senior rounds and answered badly in most senior rounds. It is not a question about vendor product names — it is a question about failure domains: what is the largest thing that can die without taking the service with it. Draw it as nested dashed boxes; the nesting is the answer.

users GeoDNS CDN PoPs REGION · us-east-1 · ACTIVE VPC 10.0.0.0/16 — private by default public subnet ALB TLS terminates here spans both zones ZONE a private subnet node group · ×6 pod pod pod data subnet primary cache ZONE b node group · ×6 pod pod pod standby cache cross-AZ write +0.5 ms sync replication zone loss = failover, zero data loss REGION · us-west-2 · DR ALB node group · scaled to 0 replica only after DNS failover async replication RPO seconds · RTO minutes OBJECT STORE — a regional service, already replicated across every zone. Blobs, backups, logs, cold data, and anything over ~100 KB.
The failure domains, nested. Pod → node → zone → region, each one survivable by the layer above it. The two arrows worth drawing are the red cross-zone write and the dashed cross-region replication, because those are the two that cost you something.

Vocabulary to have straight: RPO (recovery point objective) is how much data you can lose; RTO (recovery time objective) is how long you can be down. Warm standby optimizes cost; active-active optimizes RTO; pilot light sits between them.

The read path

Reads are a funnel. Every layer is cheaper and staler than the one behind it, and the design question is always how much staleness buys how much cost.

client edge CDN, TTL 60s in-process LRU, 1–5s shared cache Redis-class store 100% 60% 45% 3% traffic surviving each layer — write these numbers, they justify every box stale up to 60s per-box divergence stampede · hot key the truth Each layer is cheaper and staler than the next. If the product can't tolerate the staleness at a layer, that layer does not exist — say which layers you deleted and why, because deleting one is as strong a move as adding one.
Reads are a funnel, not a lookup. The percentages are the argument. A layer that sheds 2% of traffic is a layer you should delete rather than operate.

The percentages are the argument. A layer that sheds 2% of traffic is a layer you should delete rather than operate — and saying you deleted a layer is as strong a move as adding one.

The write path and the outbox

The organizing idea is a line across the diagram: above it, work the user waits for; below it, work someone else does later. Getting as much as possible below that line is most of what “make it fast” means in practice.

client service db insert outbox row same transaction — this is how you avoid the dual write ↑ the user is waiting for all of this — keep it to one round trip if you can ↓ the user is already gone — latency here is lag, not latency relay polls the outbox log · partitioned by user_id search index cache invalidation analytics load at-least-once delivery, so every consumer is idempotent dedupe on (event_id) · DLQ after N attempts · ordering is per-partition, never global — partition by the entity whose order you care about
The outbox, drawn. You cannot atomically write to your database and publish to a log. Writing the event as a row in the same transaction, then relaying it, is the standard fix — and it is the single most useful pattern to be able to draw from memory.

Shard and replicate

Shards go across, replicas go down. Drawing the grid makes the distinction impossible to blur.

route on hash(user_id) leader leader leader leader follower follower shard 0 shard 1 shard 2 shard 3 one tenant = 30% of writes → even hashing does not save you ← sharding buys writes replication buys reads & survival →
The grid: shards across, replicas down. The red shard is the observation that separates candidates — hash sharding is even in expectation, and expectation is not what a large tenant obeys.

Fan-out: how to draw a trade-off

When choosing between two designs, do not describe them in sequence. Draw both side by side and point at the one edge that differs, so the interviewer can see what they are choosing between.

FAN-OUT ON WRITE author svc posts fan-out timeline[u1] timeline[u2] timeline[uN] N writes per post reader 1 read Cheap reads, expensive writes. Breaks on celebrities: 40M copies per post. FAN-OUT ON READ author svc posts 1 write. That is all. reader svc N queries at read time, merged and sorted Cheap writes, expensive reads. p99 is now the slowest of N queries.
The same system, one edge moved. Everything else is identical — which is what makes the comparison legible. The real answer is usually hybrid: fan out on write for normal accounts, read-time merge for the few thousand accounts with huge followings.

The real answer is almost always hybrid: fan out on write for normal accounts, merge at read time for the few thousand accounts with enormous followings. The celebrity case is not an edge case; it is the design.

Service boundaries and multi-tenancy

Monolith or microservices

The honest senior answer: start with a well-structured monolith and extract services when a specific pressure demands it — independent scaling, independent deploy cadence, team ownership, or a genuinely different resource profile. Splitting for its own sake converts local function calls into network calls that can fail, and turns one transaction into a saga.

Costs to name if you propose services: distributed tracing becomes mandatory; every call needs a timeout, a retry policy and a circuit breaker; you inherit versioning and backwards compatibility; and cross-service data consistency is now your problem.

CQRS and event sourcing

CQRS separates the write model from one or more read models, each shaped for its query and updated asynchronously from the write side. Reach for it when read and write patterns diverge sharply; the cost is eventual consistency between them and more moving parts.

Event sourcing stores the sequence of changes as the source of truth and derives current state by replaying it. You get a perfect audit log, time travel and rebuildable projections; you pay with schema evolution of old events, snapshotting for performance, and the fact that “just query the current state” is no longer simple. Propose it only when auditability or replay is an actual requirement.

Multi-tenancy

Three isolation levels, in ascending cost: shared everything with a tenant id column (cheapest, and one bug leaks data across tenants); shared infrastructure, separate schemas or databases; dedicated infrastructure per tenant (simplest isolation story, worst economics, usually reserved for the largest customers). Noisy neighbours are the recurring failure: per-tenant rate limits, per-tenant concurrency caps, and weighted fair queueing so one tenant's burst cannot starve everyone else. Say this unprompted in any B2B design.

Reliability patterns

PatternWhat it doesThe detail that matters
TimeoutBounds how long you wait on a dependencyThe one everyone forgets. No timeout means one slow dependency exhausts your threads. Budget them so the sum is under your own SLO
Retry with backoff + jitterRides out transient failuresJitter is not optional — synchronized retries are a self-inflicted DDoS. Only retry idempotent operations. Cap total attempts
Circuit breakerStops calling a failing dependency; fails fastHalf-open state to probe recovery. Prevents a slow dependency from consuming every caller thread
BulkheadIsolates resource pools per dependencyOne slow downstream can't consume the thread pool the rest of the app needs
Load sheddingRejects excess work at the edgeShed cheaply and early; prioritize by request class. A fast 503 beats a slow timeout
Graceful degradationServe a worse answer rather than noneStale cache, default recommendations, hide the non-critical widget
BackpressureSignals upstream to slow downBounded queues everywhere. Unbounded buffering converts a latency problem into an outage
IdempotencyMakes retries safeClient-supplied request id with a uniqueness constraint

Blast radius: answering “what happens when X dies?”

Point at your existing diagram and annotate — a cross on the box, an arrow showing where traffic goes instead, one line on what the user sees. Do not redraw.

What diesWhat the user seesWhat you say
One instanceNothingHealth check removes it in seconds; capacity headroom absorbs it
The cacheSlow, then possibly down“That's 30× load on the store, so I need single-flight, load shedding, and headroom or the cold start kills me”
The database leaderWrites fail 10–30 sPromote the standby; reads continue from followers. Name the failover window
One zoneNothing, if provisionedThe surviving zone must already have the capacity — that's the cost
A downstream dependencyYour choiceCircuit breaker plus timeout; degrade to stale rather than error
The queue backs upStale derived dataLag alarm, DLQ for poison messages — and the sync path is unaffected, which is why it's below the line

Observability and SLOs

Volunteering this material separates people who have operated systems from people who have only designed them.

Security, in the amount a design round wants

Deployment and schema change

Rolling replaces instances gradually — cheapest, and two versions run at once, so your API and schema must be compatible with both. Blue-green keeps a full second environment and flips traffic — instant rollback, double the cost. Canary sends a small percentage to the new version and watches error rates before proceeding — the usual answer at scale. Feature flags separate deploying code from releasing behaviour, which is what makes fast rollback possible without a redeploy.

Schema migration on a live system — expand, migrate, contract: add the new column or table (compatible with old code); deploy code that writes both old and new; backfill existing rows in batches; switch reads to the new; then remove the old. Four deploys, no downtime, every step reversible. Volunteering this sequence is unusual and reads as real production experience.

Part 6

Twelve worked designs

Reading these does close to nothing. Do them standing up, with a marker, talking. The gap between “I understand this” and “I can say this in order under mild stress” is the entire skill. Each one below gives the scoping questions that change the answer, the numbers, the architecture, and the moment the round is actually won.

1 · Distributed rate limiter

Why start here: small enough to finish in 45 minutes, and it contains most of the reusable ideas.

Scoping. Per user, per IP, or per endpoint? Hard limit or soft? Distributed across many servers — assume yes, that is the whole problem. And the question that earns the first good signal: fail open or fail closed if the limiter itself is down? Almost always fail open — do not let your limiter take down your API.

Scale. 1M API keys, 10K QPS, the decision must add under 5 ms.

Interface. allow(key, cost=1) → (bool, retry_after).

Algorithm. Know all four (Part 4). Token bucket if bursts should be permitted; sliding window counter if they shouldn't.

api api api ×40 counters key:window → int atomic via Lua — otherwise two servers both read 99 and both write 100 now on the hot path of every request: 5 ms timeout, fail OPEN, local fallback Token bucket: refill R/sec up to cap C. O(1) memory, permits deliberate bursts. Response: 429 + Retry-After + X-RateLimit-Remaining. Clients back off with jitter, or you get a thundering retry at exactly the same instant. Hot tenant: split their counter into K sub-counters, sum, accept slight over-admission.
The graded moment is the race. Two servers read 99 concurrently and both write 100, letting 101 through. Naming that unprompted, then fixing it with an atomic script, is most of the round.

The graded moment is the race condition: two servers both read 99 and both write 100, letting 101 through. Fix with an atomic read-modify-write — a Lua script or equivalent server-side operation. Naming that race unprompted is most of the round.

Deep dive. The counter store is now on the hot path of every request, so: tight timeout, fail open, and optionally a local approximate fallback. At 10K QPS a network hop per request is expensive — the alternative is each server enforcing N/num_servers locally with zero coordination, which is approximate and breaks under uneven load. Name the trade and pick. Hot tenant: split their counter into K sub-counters and sum, accepting slight over-admission. Response: 429 with Retry-After, and clients back off with jitter.

2 · URL shortener

Scoping. Custom aliases? Expiry? Analytics? Who can delete? Massively read-heavy — say that early, it drives everything.

Scale. 100M new URLs/month → ~40 writes/sec. Reads at 100:1 → ~4,000 reads/sec, peak ~20K. 100M × 500 bytes × 5 years ≈ 3 TB. Small data, high read rate — this is a caching problem, not a storage problem.

Key generation is the actual design question. Three options: hash the URL (deterministic, dedupes, needs collision handling); a global counter base-62 encoded (no collisions, but a coordination bottleneck and the ids are enumerable); or — the answer that shows you think about coordination cost — each app server leases a block of 10,000 ids from a central allocator and hands them out locally: one coordination call per 10,000 URLs instead of per URL. Seven base-62 characters is 627 ≈ 3.5 trillion keys.

Storage. Key-value: short_code → (long_url, owner, created_at, expires_at), partitioned by hash of short_code. No joins needed, so a KV store beats relational here.

Read path. CDN → shared cache (hit rate is enormous; the popular 1% of links are most of the traffic) → store. Return 301 if you don't need analytics and 302 if you do — a 301 is cached by the browser and you stop seeing the clicks. That detail lands well.

Deep dive. Analytics as fire-and-forget events into a log, aggregated asynchronously — never block the redirect on counting. Expiry by TTL plus a background sweeper. Abuse: rate-limit creation, scan destinations against a malware list, and support takedown.

3 · News feed / timeline

Scoping. How fresh must the timeline be? How large can a following get? Chronological or ranked? Can we drop posts, or must delivery be complete?

Scale. 300M DAU, 2 posts/day → 600M writes/day ≈ 7K writes/sec. Reads: 20 feed loads/day → 6B reads/day ≈ 70K reads/sec, peak ~300K. Read-dominated by ~50:1.

The core decision is fan-out on write versus fan-out on read (Part 5). Fan-out on write precomputes one timeline per user: cheap reads, expensive writes, and it explodes for accounts with 40M followers. Fan-out on read stores each post once and merges at read time: cheap writes, and p99 becomes the slowest of N queries.

The answer is hybrid, and saying so with the threshold named is the point: precompute for normal accounts; for the few thousand accounts above some follower count, don't fan out — merge their recent posts into the timeline at read time. Two paths, one merge step.

Storage. posts partitioned by post_id; timeline[user_id] as a capped list of the most recent few hundred post ids in a cache, with a durable backing store; follows as an adjacency list partitioned by follower.

Deep dive. The timeline cache holds ids, not post bodies — hydrate bodies from a separate cache so an edited or deleted post doesn't need rewriting into millions of timelines. Ranking as an offline scoring pipeline whose output is a feature store the serving path reads. Backfill for a new follow. And the failure mode to name: fan-out lag under a traffic spike means some users see a stale feed — which is acceptable, and saying so explicitly is the trade-off answer.

4 · Chat / messaging

Scoping. One-to-one only or group? Group size cap? Delivery receipts and read receipts? Message history retention? End-to-end encryption? Online presence?

Scale. 50M DAU, 40 messages/day → 2B/day ≈ 25K writes/sec, peak 100K. Connections are the other capacity dimension: 50M concurrent WebSockets at ~10K per box is 5,000 gateway machines — do that arithmetic out loud, it is unusual and impressive.

Architecture. Clients hold WebSocket connections to a stateless-ish gateway tier. A session registry maps user_id → gateway_id so a message can be routed to the right connection. The chat service persists the message first, then publishes to the recipient's gateway; if the recipient is offline it goes to a push-notification path and waits in their inbox.

Data model. messages(channel_id, seq, sender, body, ts) partitioned by channel_id and clustered by seq — every read is “the last N messages in this channel,” so this key makes the common query a single sequential scan. A per-channel monotonically increasing sequence number gives ordering without trusting clocks.

Deep dive — the two hard parts. Ordering: assign the sequence number server-side at the channel's partition, so all participants agree; client timestamps are unusable. Exactly-once display: the client generates a message id, the server dedupes on it, and the client reconciles on reconnect by asking for everything after its last known sequence number — at-least-once delivery plus idempotent display. Group fan-out is the feed problem again: fan out on write for small groups, read-time merge for very large ones. Presence is best-effort with a TTL heartbeat, and it is worth saying that presence at this scale often costs more than messaging.

5 · Notification service

Scoping. Which channels — push, email, SMS, in-app? Transactional or marketing (different latency and compliance requirements)? Are user preferences and quiet hours in scope? What are the delivery guarantees?

Scale. 100M notifications/day ≈ 1.2K/sec average, but campaigns arrive as bursts of millions in a minute — the burst, not the average, is what you design for.

Architecture. Producers publish an intent to a log. A preference and eligibility service filters (unsubscribed, quiet hours, rate caps per user). A templating step renders per channel. Then per-channel worker pools with their own queues and their own rate limits, because each third-party provider has different throughput and failure behaviour. A delivery status store closes the loop with provider webhooks.

Deep dive. Isolation: separate queues per channel and per priority, so a marketing blast cannot delay a password reset — this is the single most important structural decision. Idempotency: dedupe on a notification id so a retry doesn't send twice; users notice duplicates immediately. Third-party failure: circuit breaker per provider with failover to a secondary, and a DLQ. Backpressure: a campaign of 10M is chunked and admitted at a controlled rate rather than dumped into the queue. Fan-out storms: cap per-user notification rate and coalesce ("3 people liked your post") — both a product feature and a load-shedding mechanism.

6 · Distributed job scheduler with dependencies

The richest prompt on this list and the one that rewards having operated something.

Scoping. Users submit DAGs of tasks; run each task after its dependencies succeed; retry on failure; survive a worker dying mid-task. Scheduled (cron) and event-triggered runs. Out of scope for v1: cross-DAG dependencies and backfills.

Scale. 100K DAGs, 10M task executions/day → ~120/sec average, ~1,000/sec peak. Tasks run from seconds to hours — that range is why leases exist.

Data model. dag_definitions(dag_id, spec, schedule); runs(run_id, dag_id, state, started_at); tasks(run_id, task_id, state, attempt, worker_id, lease_expires_at) partitioned by run_id; task_deps(run_id, task_id, depends_on).

a b c d submitted DAG cycle check at submit, not at run scheduler leader-elected metadata the source of truth ready queue enqueue when in-degree hits 0 worker worker worker heartbeat · renew lease · report result A slow worker is not a dead worker. The lease expires while it is still running, you reschedule, and now two copies are writing. Fence with a monotonically increasing token; downstream rejects anything holding a stale one. Retries: backoff with jitter, max attempts, then a terminal FAILED that propagates as UPSTREAM_FAILED — never leave children pending forever.
The dependency logic is Kahn's algorithm. Keep an in-degree per task, decrement as each parent succeeds, enqueue at zero. Saying that turns an abstract design into something you have obviously implemented.

The dependency logic is Kahn's algorithm: keep an in-degree per task, decrement as each parent succeeds, enqueue at zero. Saying that turns an abstract design into something you have obviously implemented. Cycle detection happens at submit time — run Kahn's and if you don't drain every node, there's a cycle.

Deep dive — where the round is won. Exactly-once execution is impossible; at-least-once plus idempotency is the real answer, because a worker can finish and die before reporting. Worker death: leases with expiry, reclaimed when heartbeats stop — and the risk you must name is that a merely slow worker is still running while you reschedule, so fence with a monotonic token. Scheduler HA: single leader via a consensus service; the same fencing idea prevents split-brain double-scheduling. Fairness: per-tenant queues with concurrency caps so one customer's 100K tasks can't starve everyone. Retries: backoff with jitter, a max-attempts cap, then a terminal FAILED that propagates downstream as UPSTREAM_FAILED rather than leaving children pending forever. The bottleneck: the metadata database, since every state transition is a write — batch updates, partition by run_id, keep the hot ready-set in memory with the database as durable backing.

7 · Metrics, monitoring and alerting

Scoping. Ingest metrics from 100K hosts; query recent data interactively and historical data for dashboards; users define threshold alerts and get notified. Recent means seconds of freshness; old means downsampled.

Scale. 100K hosts × 1,000 metrics each, sampled every 10 s = 10M data points/sec. That number alone dictates the design — say it early.

Write path. Append-only, and it must never do a random read. Agents push to an ingest tier → a durable log for buffering → an in-memory write buffer → periodic flush of immutable, compressed, columnar blocks to object storage. Time-based partitioning, because every query is a time-range query; accept that the current partition is a hot shard — it is designed for.

Retention. Raw for 7 days, one-minute rollups for 90 days, one-hour rollups for 2 years. Without downsampling, storage is unbounded.

Query path. An inverted index from label → series ids, then fetch time ranges for those series and aggregate.

Alerting. A rule evaluator that periodically runs each user's query over the recent window; matches produce an alert state transition, which goes to a notification path (design 5). The details that matter: deduplication and grouping so one bad deploy doesn't send 500 pages; hysteresis ("firing for 5 minutes") so a single spike doesn't page anyone; silences during maintenance; and evaluation sharded by rule so it scales horizontally.

Deep dive. Cardinality is the killer — every distinct label combination is a separate series, and adding a user_id label turns 1,000 series into 10 million and takes the system down. Raise it unprompted; it is the failure mode every practitioner has lived through. Also: you cannot compute a true p99 by averaging p99s across hosts, so store histograms or t-digests and merge those instead.

8 · Distributed key-value store

The “prove you know the fundamentals” prompt. It is really a checklist of Part 3.

Deep dive options: the read path with bloom filters and compaction strategy (size-tiered favours writes, levelled favours reads); how a range scan works when keys are hashed (it doesn't — you need an ordered partitioner, and that reintroduces hot shards); and how you add a node without a latency spike (throttled streaming of partitions, serve from the old owner until handoff completes).

9 · Typeahead / autocomplete

Scoping. Prefix matching only, or fuzzy? Personalized? How fresh must new terms be — minutes or days? Multi-language?

Scale. Every keystroke is a request: 10M searches/day with 20 keystrokes each is 200M requests/day ≈ 2.3K/sec average, and it must return in under 100 ms or the feature feels broken. Client-side debouncing (fire after ~50 ms of no typing) is a legitimate part of the design — mention it, because it cuts load by more than half.

Architecture. Two decoupled halves. Offline: aggregate query logs into term frequencies over a rolling window, build a trie with the top-K completions precomputed and stored at every node, and ship that structure to the serving tier. Online: a read-only in-memory lookup — walk the prefix, return the stored list. No computation at request time at all.

Deep dive. The trie is large, so shard it by prefix (all queries beginning “ca” live together) and serve from memory at the edge. Rebuild cadence is the freshness/cost trade — hourly is usually fine, with a small hot-terms overlay for breaking events. Typo tolerance via edit-distance expansion on short prefixes, which is expensive, so cap it. Filtering for offensive or unsafe suggestions belongs in the offline build, not the serving path. Personalization is a second, much smaller per-user list merged with the global one at request time.

10 · Video upload and streaming

Scoping. Upload plus playback, or playback only? Live or on-demand? What resolutions? DRM? Global audience?

Scale. The number that matters is egress, not requests: 1M concurrent viewers at 5 Mbps is 5 Tbps. No origin serves that — this is a CDN problem, and saying so in the first two minutes is the correct framing.

Upload path. Client requests a pre-signed URL and uploads directly to object storage, bypassing your servers entirely — say this explicitly, because routing multi-gigabyte uploads through an application tier is a common and expensive mistake. Chunked and resumable, because mobile connections drop. Completion publishes an event.

Transcode pipeline. A worker pool consumes upload events, splits the video into segments, transcodes segments in parallel into a ladder of bitrates, then packages into HLS/DASH with a manifest. Parallel-by-segment is what makes a two-hour film transcode in minutes. Output back to object storage; CDN pulls from there.

Playback. The player fetches the manifest, then requests segments, choosing bitrate adaptively from measured throughput and buffer level. Segments are immutable and cacheable forever, which is what makes CDN hit rates near-perfect.

Deep dive. Transcode is expensive and bursty — use a priority queue (a creator's first upload matters more than a re-encode), spot/preemptible capacity with checkpointing, and idempotent per-segment jobs so a lost worker costs one segment, not the whole video. Thumbnail and metadata extraction as separate consumers of the same event. For live, the same pipeline with much smaller segments and a latency/robustness trade you should name explicitly.

11 · Proximity / ride-hailing dispatch

Scoping. Find nearby drivers, or nearby static places? How fresh must locations be? What radius? Is matching in scope, or only search?

Scale. 1M active drivers reporting location every 4 s = 250K writes/sec of location updates — a write-heavy problem disguised as a search problem, and noticing that is the first good signal. Rider queries are far fewer, maybe 10K/sec.

The core technique: turn two-dimensional proximity into a one-dimensional key. Geohash encodes lat/long into a string where a shared prefix means spatial proximity, so “nearby” becomes a prefix range query in any ordinary key-value store. Quadtree subdivides adaptively and handles dense cities better. H3/S2 use hexagonal or spherical cells and avoid geohash's edge artifacts. Pick one and name the weakness: with geohash, two points either side of a cell boundary have completely different prefixes, so you must always query the cell and its eight neighbours.

Architecture. Location updates go to an in-memory geospatial index sharded by cell, with a durable log behind it for recovery — driver positions are cheap to lose and expensive to store, so treat them as ephemeral. Rider queries hit the index for candidate cells, then a matching service ranks by ETA (not straight-line distance) and dispatches.

Deep dive. Dense cells are hot shards by construction — a downtown cell has 100× the drivers of a suburb, so subdivide adaptively rather than using a uniform grid. Matching must avoid double-booking a driver: a short lease on the driver record while an offer is outstanding, released on decline or timeout. Location updates are the write bottleneck — batch them, and reduce reporting frequency for stationary drivers.

12 · Payments / ledger

Why do this one: it is the clearest test of whether you actually understand idempotency and consistency, and the correct answers are the opposite of most of this document's defaults.

Scoping. Internal ledger, or integration with external processors? Which currencies? Are refunds, chargebacks and holds in scope? What are the audit requirements?

The non-negotiables to state up front. Money requires strong consistency — this is the design where you choose C over A, and say so. No lost writes, ever. Full auditability. And every operation must be idempotent, because a network timeout on a payment must never mean “maybe we charged them twice.”

Data model — double-entry. Never store a mutable balance. Store immutable entries(entry_id, account_id, amount, direction, txn_id, ts) where every transaction writes at least two entries that sum to zero, and derive balances by summing (with periodic snapshots so you don't sum from the beginning of time). This is append-only, auditable, and correct under concurrency — and proposing it unprompted is a very strong signal.

Idempotency. The client supplies an idempotency key with every payment intent. Store it with a uniqueness constraint alongside the resulting transaction id; a replay returns the original result rather than performing the operation again. Keys expire after a bounded window.

Deep dive. External processors are unreliable and slow: so the flow is a state machine — pending → authorized → captured → settled — persisted at every transition, with a reconciliation job that compares your ledger to the processor's daily report and flags divergence. Never treat an HTTP timeout as a failure; treat it as unknown, and resolve it by querying the processor with your idempotency key. Cross-account transfers in one partition if possible; if not, a saga with explicit compensating entries (a reversal entry, never a deletion). Sharding by account id keeps a single account's entries together and makes its balance a local computation, at the cost of cross-account transfers spanning shards — name that trade.

Part 7

Reference and drills

The last-hour material: one page of numbers and decisions, thirty-five rapid-fire questions to test recall, a glossary, and the checklist to score your own reps against.

One-page cheat sheet

The clock

0–6 requirements · 6–10 numbers · 10–13 API · 13–17 data model · 17–27 architecture · 27–40 deep dive · 40–45 bottleneck at 10× and v1 scope.

Seven scoping questions

Users · top 2–3 features + out of scope · how big · read/write ratio · latency target · staleness tolerance · what happens when it's down. Then say the summary back.

Latency ladder

Memory 100 ns · SSD read 100 µs · DC round trip 0.5 ms · disk seek 10 ms · cross-country 70 ms · cross-Atlantic 150 ms.

Arithmetic

Day ≈ 105 s · QPS = DAU × actions / 105 · peak = 2–5× avg · storage = writes × bytes × days × RF · one box ≈ 10K QPS.

Six shapes

Rect = stateless service · cylinder = durable store · stadium = cache · ticked box = queue/log · trapezoid = router · dashed = boundary. Solid arrow = sync, dashed = async.

Draw order

Spine (3 boxes, 40 s) → label every arrow and trace one request → widen only where a number forced it → ring one box and draw its interior in the annex. Never erase.

Availability

99% = 7.2 hr/mo · 99.9% = 43 min · 99.99% = 4.3 min · 99.999% = 26 s. Dependencies multiply.

Quorum

N replicas, write W, read R. W + R > N for strong-ish reads. Consensus needs a majority, so use odd node counts.

When to add a cache

Read:write above ~10:1, or a hot subset of keys. Costs: staleness, stampedes, hot keys, and a cold-start cliff.

When to add a queue

Bursty writes, fan-out, retries, or work over ~100 ms. Costs: at-least-once delivery, so consumers must be idempotent; ordering only per partition.

When to shard

One machine can't hold the data or absorb the writes. Costs: cross-shard queries and transactions, rebalancing, and skew forever.

Always close with

“What breaks first at 10× is ___, and before that I'd ___.” Then: what you'd actually ship as v1.

Rapid-fire drills

Cover the answers. If you can't produce one in ten seconds, that's a section to reread.

1. What does a partition key decide?

Where data lives, which queries are local, and which shard gets hot. It is the most consequential decision in most designs.

2. Replication versus partitioning?

Replication is copies of the same data — buys reads and survival. Partitioning is different data on different machines — buys writes and capacity.

3. Why must queue consumers be idempotent?

Delivery is at-least-once; a consumer can process a message and die before acknowledging, so it will be redelivered.

4. Why can't you have exactly-once delivery?

The acknowledgement can be lost, and the sender cannot distinguish a lost message from a lost ack. You get at-least-once plus deduplication.

5. What is the dual-write problem and its fix?

You cannot atomically write your database and publish an event. Fix: write the event to an outbox table in the same transaction and relay it separately.

6. What breaks with last-write-wins?

Concurrent writes are silently discarded, and “last” depends on clocks you cannot trust.

7. Why is a slow worker more dangerous than a dead one?

Its lease expires while it is still running, so the work is rescheduled and two copies write. Fix with fencing tokens.

8. What does W + R > N guarantee?

The read and write quorums overlap, so a read sees at least one replica with the latest write.

9. What is a cache stampede and three fixes?

A hot key expires and everything hits the store at once. Single-flight lock; probabilistic early refresh; serve stale while refreshing. Plus jittered TTLs.

10. What does snapshot isolation still allow?

Write skew and phantoms — two transactions read the same state and each makes a decision only safe if the other didn't.

11. Why can't you average p99s?

Percentiles are not linear. Merge histograms or t-digests instead.

12. What kills a metrics system?

Cardinality. Every label combination is a separate series; a user-id label turns thousands into millions.

13. When is an LSM tree better than a B-tree?

Write-heavy workloads — sequential appends instead of random in-place updates. Cost: compaction I/O and read amplification.

14. What is a bloom filter for?

Answering “definitely absent?” cheaply, to skip a disk read. No false negatives, tunable false positives.

15. Why not hash(key) % N for sharding?

Adding a node remaps almost every key. Use a fixed large number of logical partitions, or consistent hashing.

16. What does consistent hashing actually buy?

Adding or removing a node moves only ~1/N of keys. Virtual nodes are required for even distribution.

17. Local versus global secondary index?

Local: cheap writes, scatter-gather reads. Global: single-shard reads, cross-partition writes.

18. Read-your-writes — what is it and how do you get it?

A client always sees its own writes. Pin their reads to the leader briefly, or pass a version token the replica must have reached.

19. What is PACELC and why is it more useful than CAP?

If Partition: A or C; Else: Latency or Consistency. The second half applies on every normal request, not just during failures.

20. Why is retry without jitter dangerous?

Synchronized retries produce a coordinated thundering herd on a service that is already struggling.

21. What does a circuit breaker prevent?

A slow dependency consuming every caller thread, turning one service's degradation into everyone's outage.

22. Alert on symptoms or causes?

Symptoms. Page on user-visible latency and error rate; high CPU on a healthy service is not an emergency.

23. What is an error budget?

The allowed unreliability under your SLO. It converts “ship faster versus be stable” into a quantitative decision.

24. Zero-downtime schema change?

Expand, migrate, contract: add new, dual-write, backfill, switch reads, drop old.

25. Why is the celebrity problem not an edge case?

Fan-out on write is O(followers); a single account with tens of millions makes one post a distributed job. It dictates the hybrid design.

26. Why 301 versus 302 in a URL shortener?

301 is cached by browsers, so you stop seeing clicks. Use 302 if you need analytics.

27. Why upload directly to object storage?

Routing large uploads through your application tier wastes bandwidth, memory and connections for no benefit. Use pre-signed URLs.

28. How do you make proximity search a key-value problem?

Encode 2D position into a 1D key with a shared-prefix property — geohash, quadtree, or hexagonal cells — then query the cell and its neighbours.

29. Why never store a mutable balance?

Double-entry immutable entries are auditable, concurrency-safe, and reconstructable. Derive balances, snapshot periodically.

30. What does an idempotency key actually do?

Lets the server recognize a retry and return the original result instead of performing the operation twice.

31. Where should consensus never be used?

On the data hot path. It is for metadata — leader election, membership, configuration.

32. Why is a search index never the source of truth?

It is a derived, lossy, rebuildable projection. Losing it should cost a rebuild, not data.

33. What is backpressure and what happens without it?

Signalling upstream to slow down. Without it you buffer unboundedly and convert a latency problem into an outage.

34. Two costs of multi-AZ that candidates miss?

Each zone must be provisioned for the whole load, and cross-zone calls add latency to every hop that crosses.

35. Event time versus processing time?

Events arrive late and out of order, so aggregating by arrival gives wrong answers. Use event time with watermarks.

Glossary

Anti-entropy
Background process that finds and repairs divergence between replicas, usually with Merkle trees.
Backpressure
Signalling upstream to slow down when a consumer can't keep up, instead of buffering without limit.
Bulkhead
Isolated resource pools so one failing dependency can't consume the resources others need.
CDC (change data capture)
Streaming a database's change log as events, so other systems can react without polling.
Compaction
Merging LSM files to reclaim space and reduce read amplification. Consumes I/O and causes latency spikes.
CRDT
A data type whose merge is guaranteed to converge regardless of order — used for offline-first and collaborative editing.
Dead-letter queue
Where messages go after repeated failures, so one poison message can't block the pipeline.
Fencing token
A monotonically increasing number issued with a lease; downstream rejects stale tokens, preventing two writers.
Hinted handoff
A healthy node temporarily accepts writes for a down node and forwards them on recovery.
Hot key / hot shard
One key or partition receiving a disproportionate share of traffic; the usual reason even hashing isn't enough.
Idempotent
Applying the operation twice has the same effect as applying it once.
Lease
A lock with an expiry, so a crashed holder doesn't block forever. Needs fencing to be safe.
MVCC
Multi-version concurrency control: writes create new versions, so readers never block writers.
Outbox pattern
Writing an event row in the same transaction as the state change, relayed separately — solves the dual-write problem.
Quorum
A majority (or a configured W/R) required for an operation to be considered durable or current.
Read amplification
Reading more data than requested — e.g. an LSM read touching several files.
RPO / RTO
How much data you can lose / how long you can be down after a disaster.
Saga
A sequence of local transactions with compensating actions, replacing a distributed transaction.
Scatter-gather
Fanning a query to every shard and merging results; p99 becomes the slowest shard.
Single-flight
Collapsing concurrent identical requests into one, so only the first does the work.
Split brain
Two nodes both believing they are leader. Prevented by quorum plus fencing.
Thundering herd
Many clients hitting a resource simultaneously after an expiry or restart.
Watermark
A declaration that all events up to time T are believed to have arrived, allowing a window to close.
Write amplification
Writing more bytes than the logical change requires — page splits, compaction, replication.
Write skew
Two transactions read overlapping data and each writes something only safe if the other didn't.

Lines that score

That last one is safe to say. Manufactured certainty is a far worse signal than calibrated honesty, and interviewers can tell the difference immediately.

Self-critique checklist

Score every practice rep against this. The self-scoring is where the improvement comes from.

The last one is not filler. In post-interview debriefs, “went quiet and I couldn't tell what they were thinking” sinks more otherwise-strong candidates than any technical gap. An ambiguous prompt is not a trick — it is an invitation. Treat the interviewer like a colleague you're whiteboarding with on a Tuesday afternoon, and the round mostly runs itself.

Latency and capacity figures are order-of-magnitude planning numbers, not benchmarks — use them for arithmetic, not for claims. Every diagram in this document is drawable with a straight edge and a wobbly ellipse.