Complete offline study guide · senior software engineer
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.
Part 0
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.
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
Before any technical content: what this round is, what it measures, and the twelve minutes of behaviour that decide most of the outcome.
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:
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.
Memorize this. It is your safety net when the prompt is vague and your mind is blank.
| Min | Phase | The thing you must actually do |
|---|---|---|
| 0–6 | Requirements | Ask. Write answers in the corner of the board. Say what is out of scope. |
| 6–10 | Scale | Arithmetic out loud. Land on “so roughly N QPS and M TB/year.” |
| 10–13 | Interface | Three to five operations, signature level, not implementation. |
| 13–17 | Data model | Entities, primary key, partition key. Sharding gets decided here. |
| 17–27 | Architecture | Draw it. Then trace one write and one read through the whole picture. |
| 27–40 | Deep dive | Pick the hard component unprompted. Then failure modes. |
| 40–45 | Wrap | What 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.
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.
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.
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.
Name these explicitly in the requirements phase. Most candidates forget most of them.
| Property | The question to ask yourself |
|---|---|
| Scalability | What dimension grows — users, data, or fan-out? They have different answers. |
| Availability | What's the SLO? 99.9% is 43 min/month of downtime; 99.99% is 4.3. |
| Latency | Always discuss p99, not average. Averages hide the failure. |
| Consistency | Strong, read-your-writes, or eventual? Per operation, not per system. |
| Durability | Can we ever lose an acknowledged write? Usually no — say so. |
| Cost | Storage × replication × retention. Mentioning cost reads as seniority. |
| Security | AuthN vs authZ, encryption in transit and at rest, tenant isolation. |
| Observability | What do you page on? What's the one dashboard? |
| Operability | How do you deploy this without downtime? How do you migrate the schema? |
| Failure | What it looks like | Fix |
|---|---|---|
| Diving in | Drawing boxes in minute two | Five to eight minutes on requirements. Always. |
| Silence | Long pauses while thinking | Narrate: “I'm weighing X against Y.” |
| No numbers | “It'll be pretty big” | Do the arithmetic. Ninety seconds, enormous credibility. |
| Breadth only | Fifteen boxes, none explained | Go deep on one component unprompted. |
| Buzzword salad | Naming tech without justifying it | Every technology gets “because ___” and “the cost is ___.” |
| Ignoring the hint | Steamrolling past a nudge | A hint is not a suggestion. Follow it immediately. |
| Defending a mistake | Arguing when shown a flaw | “You're right, let me revise.” Revision scores; defence doesn't. |
| Running out of clock | No deep dive, no wrap-up | Watch the time. At minute 27, move whether or not you're done. |
Part 2
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.
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.
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.
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.
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.
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.
An unlabeled arrow means “these two things are related somehow,” which is worth almost nothing. Labels are where the design lives.
writes, invalidates, polls every 30s, publishes. If you can't pick a verb, you don't know what the edge does.×12. It proves horizontal scalability and that you did the arithmetic.timeout 50ms → fail open. This is the one that reads as operational experience.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.
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.
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.
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
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.
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.
| Operation | Time | Why it matters |
|---|---|---|
| L1 cache reference | ~1 ns | Free |
| Branch mispredict | ~3 ns | Only matters in tight loops |
| Mutex lock/unlock | ~25 ns | Cheap until contended, then catastrophic |
| Main memory reference | ~100 ns | 100× slower than L1 |
| Compress 1 KB | ~2 µs | Usually 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 ms | The price of one extra service hop |
| Read 1 MB from SSD | ~1 ms | |
| Disk seek (spinning) | ~10 ms | Why random disk I/O is the enemy |
| Read 1 MB from spinning disk | ~20 ms | |
| Round trip US coast to coast | ~70 ms | Speed of light. Not negotiable. |
| Round trip cross-Atlantic | ~150 ms | When 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.
DAU × actions_per_day / 105. 10M DAU × 10 actions = 1,000 QPS average.writes/day × bytes × retention_days × replication_factor. Nearly everyone forgets the replication factor.QPS × payload. 1,000 × 10 KB = 10 MB/s (trivial). 1,000 × 10 MB = 10 GB/s (a completely different system).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.
| SLO | Downtime / month | Downtime / year | What it costs to get there |
|---|---|---|---|
| 99% — two nines | 7.2 hr | 3.65 days | One machine, best effort |
| 99.9% — three nines | 43 min | 8.8 hr | Redundancy + health checks + on-call |
| 99.99% — four nines | 4.3 min | 53 min | Multi-AZ, automated failover, canary deploys |
| 99.999% — five nines | 26 sec | 5.3 min | Multi-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.
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.
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 | Use when | Cost / caveat |
|---|---|---|
| REST / HTTP+JSON | Public APIs, broad compatibility, cacheable GETs | Verbose; no streaming; over- and under-fetching |
| gRPC / protobuf | Internal service-to-service, high volume, streaming | Binary (harder to debug); needs schema management; browser needs a proxy |
| GraphQL | Many clients with different shape needs | Query cost is unbounded unless you limit depth; caching is hard |
| WebSocket | Bidirectional, low-latency, long-lived (chat, presence) | Stateful connections → sticky routing, and connection count becomes a capacity dimension |
| Server-Sent Events | Server→client push only (feeds, notifications) | One direction; auto-reconnect is built in, which is nice |
| Long polling | Fallback when the above are unavailable | Holds 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/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.
“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.
| Model | Guarantee | Cost |
|---|---|---|
| Linearizable | Every read sees the most recent write; the system behaves as if there is one copy | Coordination on every operation; latency floor set by consensus round trips; unavailable during partitions |
| Sequential | All nodes see operations in the same order, not necessarily real-time order | Cheaper than linearizable, still needs agreement on order |
| Causal | Operations that causally depend on each other are seen in order; concurrent ones may differ | Needs causality tracking (vector clocks); often the sweet spot |
| Read-your-writes | A client always sees its own writes | Session pinning or a version token. Cheap, and usually what users actually notice |
| Monotonic reads | You never see time go backwards across successive reads | Pin a session to one replica |
| Consistent prefix | You never see an answer before its question | Ordering within a partition |
| Eventual | Replicas converge if writes stop | Nothing — and it guarantees nothing about when |
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 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.
| Scheme | How it works | What it costs |
|---|---|---|
| Leader–follower, async | Writes to one leader; followers stream the log behind it | Replication lag → a user may not read their own write. Failover can lose recent writes. |
| Leader–follower, sync | Leader waits for a follower to acknowledge before committing | Write latency now includes the slowest follower; if it's down, writes stall |
| Semi-sync | Wait for one of N followers | The usual practical compromise: no data loss on single-node failure, bounded latency |
| Multi-leader | Writes accepted in multiple regions, replicated both ways | Write conflicts you must resolve. Last-write-wins silently loses data |
| Leaderless / quorum | N replicas; write to W, read from R. If W + R > N reads see the latest write | Tunable 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.
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.
| Scheme | Good at | Bad at |
|---|---|---|
| Range (by time, by A–Z) | Range scans are cheap and local | Hot shard — today's partition takes every write |
| Hash | Even distribution in expectation | Range queries must fan out to every shard; resharding moves everything |
| Consistent hashing | Adding or removing a node moves only ~1/N of keys | More complex; needs virtual nodes to be even at all |
| Directory / lookup table | Total flexibility, easy rebalancing, can isolate big tenants | The directory is now a critical dependency on the hot path |
| Composite (tenant + hash) | Isolates tenants and spreads within them | Two decisions to get right instead of one |
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.
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.
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.
| Level | Prevents | Still allows |
|---|---|---|
| Read uncommitted | Nothing much | Dirty reads. Essentially never what you want. |
| Read committed | Dirty reads and dirty writes | Non-repeatable reads: the same row read twice in one transaction differs |
| Snapshot / repeatable read | Non-repeatable reads — the transaction sees a consistent snapshot | Write skew and phantoms: two transactions read the same state and each makes a decision that is only safe if the other didn't |
| Serializable | Everything. Result is equivalent to some serial order | Nothing — 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.
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.
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.
| Semantic | Reality |
|---|---|
| At-most-once | Fire and forget. Messages can be lost. Fine for metrics samples, never for money. |
| At-least-once | The default everywhere. Retries can duplicate. Consumers must be idempotent. |
| Exactly-once | Impossible 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. |
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 is how a set of machines agree on one value despite failures. In an interview you need the shape, not the proof.
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.
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.
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
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 ___.”
| Block | Reach for it when | What it costs |
|---|---|---|
| DNS / anycast | Route users to the nearest or healthiest region | Slow to change (TTL); not a real failover mechanism on its own |
| CDN | Static assets, cacheable responses, global users, large payloads | Invalidation is hard; useless for personalized content; another cache to reason about |
| Load balancer | More than one instance of anything | L4 is fast and TCP-level; L7 can route on path or header but costs CPU and terminates TLS |
| API gateway | Auth, rate limiting, quotas, routing belong in one place | A shared component on every hot path is a correlated failure and a deploy bottleneck |
| Reverse proxy / sidecar | mTLS, retries, circuit breaking without touching app code | Operational complexity; another hop; a whole control plane to run |
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.
| Algorithm | How | Trade-off |
|---|---|---|
| Fixed window | A 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 log | Store every timestamp; count those inside the window | Exact. Memory grows with request rate |
| Sliding window counter | Weighted blend of the current and previous window | Near-exact, O(1) memory. Usually the right answer |
| Token bucket | Tokens refill at rate R up to capacity C; a request costs one | O(1), and it permits controlled bursts — usually what an API actually wants |
| Leaky bucket | Requests queue and drain at a fixed rate | Smooths output perfectly; adds queueing latency |
The highest-value block to know deeply, because caching questions have crisp right answers and most candidates only know one strategy.
| Strategy | How | Trade-off |
|---|---|---|
| Cache-aside (lazy) | App checks cache; on miss reads the store and populates | Simple and most common. First request is always slow; stale data possible; cache and store can diverge |
| Read-through | The cache itself loads on miss | Cleaner app code; the cache becomes a dependency of correctness |
| Write-through | Write hits cache and store together | Cache is always fresh; every write is slower |
| Write-behind | Write to cache, flush to store asynchronously | Very fast writes; you can lose data on a crash |
| Refresh-ahead | Proactively refresh hot keys before expiry | Hides latency for predictable keys; wasted work for the rest |
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.
| Shape | Reach for it when | What you give up |
|---|---|---|
| Relational | Default. Joins, transactions, secondary indexes, strong consistency | Write ceiling of one machine; schema migrations on huge tables are painful |
| Key-value | O(1) access by a known key at enormous scale | No joins, no ad-hoc queries, no range scans unless designed in |
| Wide-column | Very high write throughput, tunable consistency, time-ordered rows | You design a table per query; you own compaction and repair |
| Document | Nested objects, flexible or evolving schema | Weaker consistency and join stories; easy to denormalize into a corner |
| Time-series | Append-heavy, always queried by time range, downsampled over age | Single-purpose; cardinality becomes the failure mode |
| Graph | Traversals many hops deep are the primary query | Harder to shard; a narrower operational ecosystem |
| Search index | Full text, faceting, relevance ranking | It is an index, never the source of truth. Say this — it is a common trap |
| Object store | Blobs over ~100 KB, backups, logs, data-lake files | No 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 | LSM tree | |
|---|---|---|
| Writes | In-place update, random I/O, write-ahead log for durability | Sequential append to an in-memory table, flushed to immutable sorted files |
| Reads | Predictable: one tree traversal | May touch several files; bloom filters skip most of them |
| Strength | Read-heavy, point lookups, strong transactional support | Write throughput, better compression, no in-place mutation |
| Cost | Write amplification from page splits; random write I/O | Compaction 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.
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.
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 queue | Durable log | |
|---|---|---|
| Model | A message is delivered to one consumer and removed | An append-only sequence; consumers track their own offset |
| Replay | Gone once acknowledged | Replayable — reprocess history by rewinding the offset |
| Fan-out | Needs a topic/exchange per consumer | Native: many independent consumer groups read the same log |
| Ordering | Usually best-effort | Strict per partition, never global |
| Best for | Task distribution, jobs, RPC-ish work | Event streams, CDC, pipelines, rebuilding derived state |
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.
Inverted index: term → the list of document ids containing it. Queries intersect posting lists. Add positions for phrase queries. Ranking is typically BM25 (term frequency, inverse document frequency, length normalization) plus business signals, and in modern systems a vector/embedding retrieval stage combined with keyword retrieval.
Typeahead / autocomplete: a trie of prefixes with the top-K completions precomputed and cached at each node, rebuilt offline from query logs. Serve from memory at the edge. The interesting parts are cache invalidation cadence, personalization, and typo tolerance (edit distance or fuzzy matching on the trie).
Columnar storage for analytics: store values column by column, so a query reads only the columns it touches and compresses far better because a column is homogeneous. Partition pruning: keep min/max metadata per column per chunk so a query can prove a chunk is irrelevant and skip reading it — if you independently propose this, say out loud that you have just re-derived how analytical stores work. Separation of storage and compute: data sits in object storage; independent compute clusters read it, scale to zero, and cannot contend with each other.
Reach for these the moment someone says “now there are a billion elements and you have one gigabyte of RAM.”
| Structure | Answers | Error |
|---|---|---|
| Bloom filter | “Is this key definitely absent?” | No false negatives; tunable false positives. Cannot delete (use a counting variant) |
| HyperLogLog | Approximate distinct count | ~2% error in a few KB instead of GB. Mergeable across shards |
| Count-min sketch | Approximate frequency — heavy hitters, hot-key detection | Overcounts, never undercounts |
| t-digest / histogram | Percentiles over a stream | Approximate — but you cannot average percentiles, so this is the only correct way to aggregate p99 across hosts |
| Reservoir sampling | A uniform sample from a stream of unknown length | Sampling error only |
“You can't average percentiles” is a small, extremely reliable credibility win in any monitoring or metrics discussion.
Part 5
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.
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.
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.
“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.
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.
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.
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 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.
Shards go across, replicas go down. Drawing the grid makes the distinction impossible to blur.
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.
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.
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 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.
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.
| Pattern | What it does | The detail that matters |
|---|---|---|
| Timeout | Bounds how long you wait on a dependency | The 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 + jitter | Rides out transient failures | Jitter is not optional — synchronized retries are a self-inflicted DDoS. Only retry idempotent operations. Cap total attempts |
| Circuit breaker | Stops calling a failing dependency; fails fast | Half-open state to probe recovery. Prevents a slow dependency from consuming every caller thread |
| Bulkhead | Isolates resource pools per dependency | One slow downstream can't consume the thread pool the rest of the app needs |
| Load shedding | Rejects excess work at the edge | Shed cheaply and early; prioritize by request class. A fast 503 beats a slow timeout |
| Graceful degradation | Serve a worse answer rather than none | Stale cache, default recommendations, hide the non-critical widget |
| Backpressure | Signals upstream to slow down | Bounded queues everywhere. Unbounded buffering converts a latency problem into an outage |
| Idempotency | Makes retries safe | Client-supplied request id with a uniqueness constraint |
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 dies | What the user sees | What you say |
|---|---|---|
| One instance | Nothing | Health check removes it in seconds; capacity headroom absorbs it |
| The cache | Slow, 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 leader | Writes fail 10–30 s | Promote the standby; reads continue from followers. Name the failover window |
| One zone | Nothing, if provisioned | The surviving zone must already have the capacity — that's the cost |
| A downstream dependency | Your choice | Circuit breaker plus timeout; degrade to stale rather than error |
| The queue backs up | Stale derived data | Lag alarm, DLQ for poison messages — and the sync path is unaffected, which is why it's below the line |
Volunteering this material separates people who have operated systems from people who have only designed them.
user_id label turns a thousand series into ten million and takes the system down. Raise this unprompted in any monitoring design.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
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
The “prove you know the fundamentals” prompt. It is really a checklist of Part 3.
W + R > N gives strong-ish reads; W=1 gives fast writes and weak reads.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).
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.
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.
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.
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
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.
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.
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.
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.
Day ≈ 105 s · QPS = DAU × actions / 105 · peak = 2–5× avg · storage = writes × bytes × days × RF · one box ≈ 10K QPS.
Rect = stateless service · cylinder = durable store · stadium = cache · ticked box = queue/log · trapezoid = router · dashed = boundary. Solid arrow = sync, dashed = async.
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.
99% = 7.2 hr/mo · 99.9% = 43 min · 99.99% = 4.3 min · 99.999% = 26 s. Dependencies multiply.
N replicas, write W, read R. W + R > N for strong-ish reads. Consensus needs a majority, so use odd node counts.
Read:write above ~10:1, or a hot subset of keys. Costs: staleness, stampedes, hot keys, and a cold-start cliff.
Bursty writes, fan-out, retries, or work over ~100 ms. Costs: at-least-once delivery, so consumers must be idempotent; ordering only per partition.
One machine can't hold the data or absorb the writes. Costs: cross-shard queries and transactions, rebalancing, and skew forever.
“What breaks first at 10× is ___, and before that I'd ___.” Then: what you'd actually ship as v1.
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.
That last one is safe to say. Manufactured certainty is a far worse signal than calibrated honesty, and interviewers can tell the difference immediately.
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.