Distributed systems
Every frontier-lab loop probes this, and it's where you've said you're weakest. This chapter teaches the lens (CAP/PACELC), the impossibility result (FLP), the building blocks (consensus, quorums, time, replication), and the canonical systems (Spanner, Dynamo, Kafka, S3) tightly enough that you can answer any senior-staff question with first principles instead of memorized trivia.
What you'll learn
- CAP & PACELC — the lens that organizes everything
- The impossibility result — FLP and what it forced us to do
- Consistency lattice — linearizable to eventual
- Time in distributed systems — Lamport, vector, HLC, TrueTime
- Distributed transactions — 2PC, sagas, when each works
- Consensus — Paxos vs Raft vs ZAB
- Quorums & consistent hashing — how data finds its replica
- Replication topologies — single-leader to leaderless
- CRDTs — convergence without coordination
- Failure detection — heartbeats, phi-accrual, SWIM
- Storage engines — LSM vs B-tree, the canonical choice
- The systems you must know — Spanner, Dynamo, BigTable, Kafka, S3
- Service architecture — load balancing, caching, rate limiting
- Networking quick reference — TCP, QUIC, gRPC
- The 10 must-know papers
CAP is the founding theorem of distributed systems design: when nodes can't talk, you must choose between serving possibly-stale answers or refusing to answer at all. PACELC, introduced by Daniel Abadi in 2012, fills in the missing 99% of life — what happens when there is no partition. Together they give you a 2×2 to place every database you will ever discuss in an interview.
CAP is not "pick 2 of 3." Partitions are non-negotiable in real systems, so the only real choice is: during a partition, sacrifice consistency (AP) or availability (CP)? PACELC adds the missing dimension: during normal ops, favor latency (EL) or consistency (EC)? Every major database maps to one cell of this 2×2.
Plain-words version: If two servers in your cluster can no longer communicate (network partition), you face a forced choice on every incoming request: (a) answer from possibly-stale local state — sacrificing consistency — or (b) refuse to answer until the partition heals — sacrificing availability. You cannot do both.
Concrete example: You have a key-value store on servers A and B. Client writes x=5 to A. The network link A↔B goes down. Client now asks B for x. B's choices: return x=3 (old value, AP choice) or return an error (CP choice). There is no third path.
Formal statement (Gilbert & Lynch 2002): A distributed system cannot simultaneously guarantee all three of Consistency (linearizability), Availability (every non-failing node responds), and Partition Tolerance (the system keeps operating despite message loss). Since real networks partition, P is non-negotiable — the trade-off is always C vs A during a partition.
The phrase "CAP: pick 2 of 3" is technically true but deeply misleading in practice. In a real distributed system running on commodity hardware and real networks, partitions will happen. Therefore P is not something you "pick" — it is forced on you. The real decision is: when a partition hits, which do you value more, C or A? This correction is one of the most common early filters at top-tier system design interviews.
A network partition is a rare, exceptional event. What happens the other 99.9% of the time? CAP is silent. Abadi's PACELC theorem fills that gap:
Why it matters: Most real systems sacrifice consistency for latency even with no partition at all — they avoid the synchronous cross-replica round-trip to keep reads fast. PACELC makes this explicit so you can reason about it.
Example — Dynamo (AP/EL): A write to Dynamo is acked as soon as 2 out of 3 replicas confirm (W=2, N=3). The third replica may lag. A read with R=1 could land on the lagging replica and return a stale value. There is no partition — the system is just choosing latency (skip the extra round-trip) over consistency (verifying the freshest replica).
Example — Spanner (CP/EC): A Spanner read returns only after the server confirms its Paxos lease is current. This adds latency (the commit-wait), but guarantees the reader sees the globally latest value even at the cost of ~7ms extra per transaction.
| System | Partition choice | Else choice | Mechanism |
|---|---|---|---|
| Spanner | CP | EC | TrueTime commit-wait; Paxos per shard. |
| Dynamo / Cassandra | AP | EL | Sloppy quorum; tunable R/W. |
| HBase / BigTable | CP | EC | Single tablet server per range; reads block on lease handoff. |
| MongoDB (default) | CP-ish | EC | Primary-only writes; reads can relax via readPreference. |
| CockroachDB | CP | EC | HLC + max-clock-offset; Raft per range. |
Trigger: "Walk me through CAP theorem" or "Where does [database] sit on CAP?"
- Correct the framing first: P is not optional. The real choice is CP vs AP during a partition.
- Then immediately extend to PACELC: "But CAP only covers partition time. PACELC also asks what they trade in normal operation — latency vs consistency."
- Place the specific system: "Cassandra is AP/EL — it always answers (AP), and in normal ops favors latency (EL) with tunable R+W. Spanner is CP/EC — halts during a partition (CP) and pays latency for linearizability (EC)."
- If time allows: mention Abadi 2012 as the citation.
Never: Say "you pick 2 of 3" without the partition-forced correction, or claim any production system is "CA" — there is no CA in real distributed systems with real network failures.
A single machine can be CA because it never has a network partition between two servers. But the moment you add a second machine and a network link, partitions become possible. "CA" is not a meaningful category in a multi-node distributed system. When people say "I'll sacrifice P," they are imagining a system that never has network failures — which is not a distributed system.
"Consistency" in CAP means linearizability specifically — every read returns the result of the most recent write, and operations appear to happen at a single point in real time. "Consistency" in ACID (transactions) means something completely different: the database moves from one valid state to another. These two uses of "consistency" are the single most common source of confusion in distributed-systems interviews.
- P is forced. The real CAP choice is CP vs AP during a partition only.
- PACELC covers the 99%: EL (latency wins) vs EC (consistency wins) in normal ops.
- Spanner = CP/EC. Dynamo/Cassandra = AP/EL. Place every system you mention on this 2×2.
- "Consistency" in CAP = linearizability; NOT the C in ACID.
Q1. A recruiter says "We need a CA database." What do you tell them?
Q2. You're designing a global payments system. Where on CAP/PACELC do you sit, and why?
Q3. Cassandra's documentation claims "tunable consistency." Does that mean it can be CP?
Q4. Explain PACELC with a real numbers example for Dynamo.
Q5. Why does Spanner call its guarantee "external consistency" rather than "linearizability"?
Q6. How does CockroachDB achieve CP/EC without GPS-based atomic clocks?
Q7. "BigTable is CP — why does a tablet server failure cause brief unavailability?"
Q8. What breaks if you ignore PACELC and only think about CAP?
Fischer, Lynch, and Paterson proved in 1985 that you cannot build a deterministic consensus protocol that is both safe (never decides wrong) and live (always decides eventually) in a fully asynchronous network with even a single crash failure. This is the deepest theoretical result in distributed systems. Understanding what it says — and what it doesn't say — is essential for reasoning about why Raft, Paxos, and every real system works the way it does.
FLP (Fischer, Lynch, Paterson 1985): async network + 1 crash + deterministic protocol = cannot guarantee both safety and liveness. Real systems escape with timeouts (semi-synchrony), randomization, or by deliberately sacrificing liveness during pathological partitions. Raft and Paxos always choose safety over liveness.
Plain words: In a fully asynchronous system, there is no upper bound on how long a message can take to arrive, and no upper bound on how long a node can take to process a request. This matters because it means you cannot tell the difference between a node that is slow and a node that has crashed.
Concrete example: You send a message to Node B at time 0. By time 100ms, you haven't heard back. Did B crash? Or is it just slow, and the message will arrive at time 101ms? In a fully asynchronous model, you have no way to know — ever. Any timeout you set is a guess, not a proof.
Why this matters for consensus: To decide a value, some nodes must commit to an outcome. But if a node might be slow (not crashed), you can never safely ignore it — it might have voted differently. And if you wait for it forever, you never decide. FLP shows this tension is not a design flaw; it is a mathematical limit.
The proof works by showing there always exists an execution where the system is in a "bivalent" state — a state where the protocol hasn't decided yet, and where both decision values (0 or 1) are still reachable depending on which messages arrive next. The key lemma: starting from any bivalent state, there is always a way to make the next step also bivalent (by carefully delaying or dropping one message from one node). Therefore, the execution can be stretched forever without deciding — while remaining safe (no wrong decision was made).
The one-crash requirement: The proof only needs one node that may have silently crashed (you can't tell it from a slow node). With zero crashes and finite message delays, consensus is easy. With even one potential crash in a truly async world, the math breaks.
What FLP does NOT say: It does not say consensus is impossible. It says consensus cannot be guaranteed to terminate in all executions. In practice, the "bad" executions are adversarially constructed and never arise organically — hence why Raft and Paxos work fine in the real world.
- Failure detectors / timeouts (semi-synchrony) — assume that messages usually arrive within some bound δ. If a node hasn't responded in 2δ, declare it dead and proceed. This assumption is false in an adversarial network but true enough in practice. Raft uses randomized election timeouts (150–300ms); Paxos relies on a similar practical bound. The theoretical justification is Chandra & Toueg's eventually perfect failure detectors (1996): a detector that may make mistakes early but is eventually accurate.
- Randomization — Ben-Or (1983) showed that adding a coin flip to consensus breaks symmetry. No adversary can keep the system bivalent forever because the coin flip introduces outcomes the adversary cannot predict. Expected (not worst-case) termination is guaranteed. Used in Ben-Or's protocol and later in probabilistic BFT protocols.
- Accept rare liveness loss — Raft and Paxos choose safety over liveness when forced. During a genuine split-brain (two equal-sized partitions, neither a majority), Raft simply stops electing a leader and stops committing. No safety violation occurs; the cluster just stalls. Once the partition heals, liveness resumes. This is a conscious design choice: it is better to halt than to make an inconsistent decision.
FLP says there exist executions where a deterministic protocol does not terminate. In Raft, this would require adversarially timed election splits (two candidates each getting exactly half the votes every time, forever). In practice this is vanishingly rare and the randomized timeout breaks it within one election cycle. Raft is live under the eventual leader property: with high probability, one node wins the election within a few rounds.
It doesn't avoid FLP — it sidesteps it by adding an assumption FLP excludes. FLP proves impossibility in a fully asynchronous model with zero timing guarantees. Failure detectors introduce partial synchrony: "messages eventually arrive within some bound." This is outside FLP's scope. You're not defeating FLP; you're working in a different (and more realistic) model.
Trigger: "What is FLP impossibility?" or "Why can't you guarantee consensus in distributed systems?"
- State the result precisely: async + 1 crash + deterministic = no protocol guarantees both safety and liveness.
- Explain the model: "fully asynchronous" means no message-delay bounds, so you can't distinguish crash from slow.
- Name the three escapes: timeouts (semi-synchrony), randomization, or sacrifice liveness.
- Anchor to a real system: "Raft uses randomized timeouts — it picks the practical escape of semi-synchrony. When a genuine split-brain occurs, it stops committing (loses liveness) rather than risk safety."
Never: Say "FLP means you can't have consensus in practice" — you absolutely can, just not with a watertight termination proof in an adversarial async model.
- FLP: fully async + 1 crash + deterministic protocol = no guarantee of both safety AND liveness.
- The key: you can't distinguish a slow node from a crashed one — any timeout is an assumption, not a proof.
- Three escapes: timeouts (Raft, Paxos), randomization (Ben-Or, probabilistic BFT), accept halting (Raft split-brain).
- Raft/Paxos: always sacrifice liveness before safety. They halt; they never give a wrong answer.
Q1. What does FLP actually prove, and what does it not prove?
Q2. Raft uses random timeouts. How does this relate to FLP?
Q3. What is the difference between safety and liveness, and why does FLP force you to sacrifice one?
Q4. What is an "eventually perfect failure detector" and how does it relate to consensus?
Q5. Can a Byzantine fault-tolerant protocol avoid FLP?
Q6. Raft halts during a split-brain. Walk through exactly what happens.
Q7. Why can you build a correct key-value store on top of Raft despite FLP?
Q8. What would a system that sacrifices safety look like, and why is this never acceptable?
Q9. How does the Ben-Or randomized protocol escape FLP?
Consistency is the most overloaded word in distributed systems: it means one thing in ACID, another in CAP, and varies across a spectrum for replicated databases. This chapter builds two distinct lattices — the replication consistency ladder (what an isolated read can see) and the transaction isolation ladder (what a transaction sees of concurrent transactions) — with concrete anomalies to make each level stick.
Two lattices: replication (linearizable → sequential → causal → eventual) and transaction isolation (serializable → snapshot isolation → read committed → read uncommitted). Strict serializable = both at once; that is Spanner's guarantee. Snapshot isolation looks safe but lets write skew through — the classic gotcha.
These models describe what a single read operation can see on a replicated system, without assuming anything about transactions.
Example: Client A writes x=5 at T=1:00:01. Client B's read that starts at T=1:00:02 must return 5 — there is no valid linearizable schedule where B reads x=3.
Cost: Requires synchronous replication or leader-quorum reads. Spanner, etcd, ZooKeeper (write path).
Anomaly allowed: Client A writes x=5 at T=1 and then tells Client B "go read x." B's read may return x=3 if it lands on a replica that hasn't caught up — even though A's write "happened first" in wall time. The total order exists; it just doesn't have to match the wall clock.
Used by: GPU memory models, early shared-memory systems; rare in distributed databases.
Example: User A posts "Check out my photo!" then posts the photo. Another user must see the photo post after the text post (causal order). But two users posting unrelated photos concurrently may appear in different orders for different observers — that's fine.
Used by: COPS (Facebook research), some Cassandra configurations, distributed social feeds.
Anomaly allowed: A read immediately after a write can return any older value, possibly from any replica. The system only guarantees convergence after quiescence.
Used by: Dynamo (R=1), Cassandra (default), DNS, CDN caches.
These models describe what a transaction can see of other concurrent transactions. They apply to databases with ACID-style transactions, not to standalone reads.
Setup: Hospital on-call rule: at least one doctor must be on call at all times. Currently Doctor A and Doctor B are both on call.
Transactions run concurrently:
-- Doctor A's transaction:
SELECT COUNT(*) FROM on_call WHERE on_duty = TRUE; -- returns 2 (A and B)
UPDATE on_call SET on_duty = FALSE WHERE doctor = 'A'; -- goes off call
-- Doctor B's transaction (same snapshot):
SELECT COUNT(*) FROM on_call WHERE on_duty = TRUE; -- also returns 2
UPDATE on_call SET on_duty = FALSE WHERE doctor = 'B'; -- also goes off call
Under SI: No write conflict — A and B update different rows. Both transactions commit. Result: zero doctors on call. The invariant is violated.
Why SI misses it: SI detects conflicts on the same row. This anomaly involves two transactions each reading a shared condition and writing to disjoint rows — a "write skew" on the predicate "at least one doctor on call."
Fix: Use SSI (Postgres "serializable" mode — uses predicate locking + dependency tracking), or add SELECT ... FOR UPDATE to the reads to create explicit row-level locks.
Strict serializable = serializable + linearizable. A transaction's effects appear in some serial order that is also consistent with real time (the serialization point falls within the transaction's real-time interval).
This is the strongest combined guarantee. It matters for multi-user systems: serializable alone allows the serial order to be reordered in counter-intuitive ways ("future transactions appear to have happened before past ones" in theory). Strict serializable eliminates this.
Spanner provides strict serializability globally — across datacenters, for cross-shard transactions, via TrueTime + 2PC + Paxos. PostgreSQL "serializable" is SSI: serializable but not linearizable (the serial order can differ from wall-clock order).
Linearizability (also called atomic consistency) is about single operations on a single object respecting real time — each operation appears instantaneous. Serializability is about groups of operations (transactions) across multiple objects having a valid serial ordering. You can have one without the other. A database can be linearizable (each read sees the latest write) but not serializable (transactions have anomalies like write skew). Or serializable but not linearizable (the serial order doesn't match wall-clock time). Strict serializable is both.
PostgreSQL's SERIALIZABLE isolation level uses SSI (Serializable Snapshot Isolation, Cahill 2008), which genuinely prevents all serializable anomalies including write skew. MySQL's SERIALIZABLE is actually Two-Phase Locking — also correct, but slower. The confusion arises because older literature (and some databases) label READ REPEATABLE as "serializable" — MySQL's REPEATABLE READ is actually Snapshot Isolation, which has write skew. Always check the actual implementation.
Trigger: "What consistency does your database give?" or "What isolation level would you use for [scenario]?"
- Distinguish the two ladders immediately: replication consistency (what can a read see?) and transaction isolation (what can a transaction see of concurrent transactions?).
- For transactions: default to Snapshot Isolation for most workloads, but flag write skew risk. If predicates are involved (like "at least one X"), use Serializable (SSI in Postgres).
- For replication: if you need linearizability, you need a CP system (Spanner, etcd, ZooKeeper); if eventual is fine, Dynamo/Cassandra suffice. Name the anomaly that eventual consistency allows for your use case.
Never: Conflate "ACID consistency" (valid state transitions) with "CAP consistency" (linearizability). They are different words that happen to share a letter.
- Replication ladder: linearizable (real-time) → sequential (program-order) → causal (causally-ordered) → eventual (converges eventually).
- Isolation ladder: serializable → snapshot isolation → read committed → read uncommitted.
- Snapshot isolation allows write skew. SSI (Postgres serializable) fixes it.
- Strict serializable = serializable + linearizable. Spanner provides this globally.
- "Consistency" in CAP = linearizability. "Consistency" in ACID = valid state. Two different words.
Q1. What is write skew and why doesn't Snapshot Isolation catch it?
Q2. Explain the difference between linearizability and serializability with concrete examples.
Q3. What consistency does Cassandra provide by default, and what anomalies can occur?
Q4. A colleague says "our system is ACID compliant, so we have no consistency problems." What's wrong?
Q5. You're building an inventory system. Under Snapshot Isolation, can you safely decrement a counter?
Q6. What is "read-your-writes" consistency and when does it break in practice?
Q7. What is the causal consistency guarantee, and which real systems provide it?
Q8. What does SSI (Serializable Snapshot Isolation) add to plain Snapshot Isolation?
Wall clocks drift. NTP corrects them but introduces ~10ms of uncertainty. At scale, 10ms is an eternity — enough for thousands of operations to interleave incorrectly. Distributed systems need a way to order events without trusting hardware clocks. This chapter covers the four clock strategies you must know: Lamport (total order, no causality), vector clocks (full causality, O(N) cost), HLC (physical anchor + logical tie-break), and TrueTime (bound the uncertainty and sleep through it).
Wall-clock (NTP ~10ms, PTP <1ms). Lamport: total order, no causality (1 counter). Vector: full causality, O(N) per event. HLC: logical + physical anchor (CockroachDB, MongoDB). TrueTime: GPS + atomic clocks bound the uncertainty interval; commit-wait sleeps through it (Spanner). Each is the right tool for a different trade-off.
Two servers synchronized with NTP can disagree by up to ~10ms (PTP: sub-millisecond in a controlled LAN, but not globally). More importantly, clocks can jump backwards after an NTP correction. A naive system that uses System.currentTimeMillis() to order events can see:
- Server A timestamps write at
1000ms; server B timestamps a later write at998ms(B's clock is 2ms behind). The "later" write appears to have happened first. - A clock jump backward on server A causes two sequential events on A to get the same or reversed timestamps.
The fundamental issue: wall-clock time gives you a number, but not a reliable ordering. Logical clocks were invented to provide ordering guarantees independent of hardware.
The rule: Each process maintains a counter L, initially 0. On any event: L = L + 1. When sending a message: attach current L. When receiving a message with timestamp T: L = max(L_local, T) + 1.
Concrete example with 3 processes:
Process A: event a1 → L=1; event a2 → L=2; sends msg to B (carries L=2)
Process B: event b1 → L=1; receives from A → L=max(1,2)+1=3; event b2 → L=4
Process C: event c1 → L=1; event c2 → L=2
What Lamport gives you: If event a happened before event b (a → b), then L(a) < L(b).
What it does NOT give you: The converse. L(a) < L(b) does NOT mean a → b. Two concurrent events (c1 and a1 in the example) still get an ordering (L(c1)=1, L(a1)=1 — a tie broken by process ID), but that ordering is artificial, not causal.
Use case: Assigning a total order to events when you only need "some" consistent ordering — e.g., log entries in a distributed system where you want to merge logs without duplicates.
The rule: Each of N processes maintains an array VC of N counters, one per process. On local event: increment your own slot. On send: attach full VC array. On receive: take element-wise max of local VC and received VC, then increment your own slot.
Causal ordering rule: VC(a) < VC(b) (element-wise) if and only if a causally precedes b (a → b). If neither VC(a) < VC(b) nor VC(b) < VC(a), the events are concurrent.
3-process example:
Process A: a1 → VC=[1,0,0]; sends to B with [1,0,0]
Process B: b1 → VC=[0,1,0]; receives from A → [max(0,1), max(1,0)+1, 0] = [1,2,0]; b2 → [1,3,0]
Process C: c1 → VC=[0,0,1]; sends to B with [0,0,1]
Process B: receives from C → [max(1,0), 3+1, max(0,1)] = [1,4,1]; b3 → [1,5,1]
From the clocks alone, you can tell: a1 → b2 (VC(a1)=[1,0,0] < VC(b2)=[1,3,0], element-wise). c1 and a1 are concurrent ([0,0,1] and [1,0,0] — neither is less).
Cost: O(N) per message (one integer per process). With 1000 processes, that's 1000 integers per message. Practical for small clusters (Dynamo uses them); impractical for large ones without compression (dotted version vectors).
Problem HLC solves: Vector clocks give causality but no physical anchor; wall clocks give physical time but no causality and may go backward. HLC (Kulkarni et al. 2014) combines both: it keeps a logical component for tie-breaking while anchoring to the physical clock, and it never goes backward.
The rule: Each node tracks (l, c) where l ≈ max observed physical time, c = logical counter for tie-breaking within the same physical millisecond. On send/receive: advance l to max(l_local, l_received, now), reset or advance c accordingly.
Bound: The l component of HLC is within a bounded offset ε of true wall time. For CockroachDB, ε = 500ms (default max-clock-offset). If a node's clock drifts beyond 500ms, it is removed from the cluster.
Users: CockroachDB (HLC + max offset for Spanner-like guarantees without atomic clocks), MongoDB (causally consistent sessions).
The insight: Instead of trying to synchronize clocks perfectly (impossible), Google's TrueTime bounds the uncertainty interval and makes the software wait until the uncertainty passes.
How it works: Every datacenter has GPS receivers + atomic clocks. TrueTime exposes a single API call: TT.now() returns an interval [earliest, latest] such that the true time is guaranteed to lie within it. The uncertainty is typically 1–7ms (driven by GPS + atomic clock drift between corrections).
Spanner commit-wait, walked through step by step:
- Transaction T1 wants to commit. Calls
TT.now()→ returns[10:00:00.000, 10:00:00.007]. Uncertainty: 7ms. - Spanner assigns commit timestamp =
latest = 10:00:00.007(safe upper bound — definitely in the future). - Spanner writes commit to the Paxos log.
- Commit-wait: Spanner sleeps until
TT.now().earliest > 10:00:00.007. This takes approximately 7ms. - Spanner releases the commit. The data is now visible.
Why this works: Any transaction T2 that starts after T1 commits will call TT.now() and get earliest > 10:00:00.007. So T2's timestamp is strictly greater than T1's commit timestamp. External consistency (T1 always happens before T2 in the committed order) holds globally without any cross-datacenter coordination.
Cost: ~7ms of extra latency per read-write transaction. Write-intensive global transactions pay this; read-only transactions using stale reads can skip it.
| Clock type | Causality | Physical anchor | Size | Used by |
|---|---|---|---|---|
| NTP/PTP | None | Yes (~10ms/<1ms) | 8 bytes | Logging, metrics |
| Lamport | Partial (→ only) | No | 8 bytes | Event ordering |
| Vector clock | Full (→ and ∥) | No | O(N) bytes | Dynamo, Riak |
| HLC | Partial + bounded physical | Yes (bounded ε) | 16 bytes | CockroachDB, MongoDB |
| TrueTime | Full (external consistency) | Yes (GPS, ~7ms) | Interval [earliest,latest] | Spanner |
Trigger: "How does Spanner achieve global consistency?" or "Why can't you just use wall-clock time?"
- Explain why wall clocks fail: NTP has ~10ms drift, clocks can go backward, so you can't use them for ordering across machines.
- Name the logical clock progression: Lamport (total order, no causality) → vector (full causality, O(N)) → HLC (practical compromise with physical anchor).
- For Spanner specifically: TrueTime gives a bounded uncertainty interval; commit-wait sleeps until the interval passes, guaranteeing external consistency without cross-datacenter coordination.
- State the cost: ~7ms extra latency per write transaction. Worth it for global linearizability.
Never: Say "Spanner uses atomic clocks for exact time" — it uses them to bound uncertainty, not to get exact time. The commit-wait is what provides the guarantee, not the clocks themselves.
- Lamport: total order only. L(a)<L(b) does NOT imply a→b.
- Vector: full causality. VC(a) < VC(b) iff a→b (element-wise). Cost: O(N).
- HLC: physical anchor + logical tie-break. CockroachDB default (max offset 500ms).
- TrueTime: GPS+atomic clocks bound uncertainty to ~7ms. Commit-wait: sleep until TT.now().earliest > commit_ts. External consistency.
Q1. Lamport says L(a) < L(b). Does this mean a happened-before b?
Q2. Vector clocks have O(N) size per message. How do real systems handle this at scale?
Q3. Walk me through Spanner's commit-wait with actual numbers.
Q4. How does CockroachDB achieve Spanner-like consistency without atomic clocks?
Q5. What is external consistency, and how does it differ from linearizability?
Q6. Why can't you use NTP + a large max-offset to approximate TrueTime cheaply?
Q7. Two processes exchange a message. Draw the vector clocks and show which events are concurrent.
Q8. In what situation would you use Lamport clocks over vector clocks?
2PC is correct but blocks if the coordinator dies after prepare. 3PC fixes blocking but assumes synchronous networks (which don't exist). Paxos Commit replicates the coordinator. Sagas drop ACID entirely — long-running flows split into compensable steps, no distributed lock. Real systems: 2PC for short cross-shard tx (Spanner does 2PC over Paxos), sagas for cross-service workflows (orders, payments).
2PC — prepare, then commit
- Coordinator sends PREPARE to all participants.
- Each participant locks resources, writes a prepare record to its log, votes YES or NO.
- If all YES, coordinator writes COMMIT to its log, sends COMMIT. If any NO or timeout, sends ABORT.
- Participants apply and release locks.
The blocking problem: if the coordinator crashes after participants vote YES but before sending COMMIT/ABORT, participants are stuck holding locks indefinitely. They cannot decide unilaterally because they don't know what the coordinator told the others.
3PC and Paxos Commit
3PC adds a pre-commit phase so participants can recover safely if the coordinator dies — but it only works under synchronous network assumptions, which is why it's almost never deployed. Paxos Commit (Gray & Lamport 2006) is the production answer: replicate the coordinator's state machine via Paxos. Spanner does this — each shard is a Paxos group, and 2PC runs across groups.
Sagas — the alternative for long flows
(Garcia-Molina & Salem 1987.) Break a long-running tx into N local transactions, each with a compensating action (refund, cancel, release inventory). On failure mid-flow, run compensations for completed steps in reverse. No distributed lock. Caveat: not isolated — other readers see intermediate states.
Use 2PC when
- Cross-shard tx within one trusted system (Spanner, CockroachDB).
- Short critical sections; locks held for ms.
- You can run a Paxos-replicated coordinator.
Use sagas when
- Cross-service workflow (order → payment → shipping).
- Steps run for seconds to days.
- You can write a compensating action for every step.
- 2PC: correct, blocks if coordinator dies after prepare. Used over Paxos in Spanner.
- 3PC: rarely deployed — assumes synchrony.
- Sagas: cross-service workflows; every step needs a compensating action.
Paxos is the original; correct, fast, infamously hard to implement. Raft (2014) is Paxos restructured for understandability — same guarantees, simpler mental model, dominant in 2020s. ZAB is ZooKeeper's variant (primary-backup with total order). Build Raft yourself in MIT 6.824 Lab 2 — it's the single highest-leverage exercise for this entire pillar.
Paxos in one paragraph
Two phases. Prepare: a proposer picks a unique number n, asks acceptors to "promise" not to accept anything < n; if it gets a majority of promises, it learns the highest-numbered already-accepted value. Accept: it asks the same majority to accept (n, value); if all promise still holds, the value is chosen. Quorum (majority) ensures any two rounds intersect. Multi-Paxos elides the prepare phase once a leader is stable — the typical case.
Raft — the modern default
Ongaro & Ousterhout, USENIX ATC 2014. Designed top-down for understandability. Splits consensus into three sub-problems: leader election, log replication, safety.
Memorize these and you can answer any Raft question
- Elect by majority with random timeouts. Followers become candidates on timeout, request votes; whoever wins a majority (in a term) is leader. Random timeouts prevent split votes.
- Replicate the log; commit by majority. Leader appends entries, sends
AppendEntriesto followers; once a majority has stored an entry, it is committed and applied to the state machine. - Leader Completeness: a leader's log contains all committed entries; only commit entries from the current term. The "only commit current-term entries" rule is the subtle one — it prevents an old leader's entry from being overwritten after re-election. It took 6 months to nail this down in the original paper.
Membership changes: joint consensus — overlap old and new configurations during the transition so any majority of old AND any majority of new must overlap, preserving safety.
Build it yourself
Implement Raft in MIT 6.824 Lab 2 (Go). The single most useful exercise for distributed-systems interviews. After Lab 2 + Lab 3 (KVStore on Raft), you can answer Raft questions from first principles instead of recall.
ZAB — ZooKeeper Atomic Broadcast
Primary-backup with total order. Like Raft, has leader election and log replication, but ZAB explicitly preserves order of all messages from the primary across reconfiguration. Used by ZooKeeper; its semantics (linearizable writes, sequentially-consistent reads, watches) are the basis for Chubby-style coordination services.
- Raft = leader election (random timeouts) + log replication (majority commit) + Leader Completeness (only commit current term).
- Multi-Paxos = Paxos with stable leader, prepare-elision; equivalent to Raft in practice.
- ZAB powers ZooKeeper. Chubby uses Multi-Paxos.
- Implement Raft yourself. There is no substitute.
Quorum math: R + W > N ⇒ every read intersects every write ⇒ strong-ish consistency. Consistent hashing puts keys and nodes on a ring; adding a node moves only K/N keys instead of nearly all of them. Jump hash (O(log n), no node IDs) and rendezvous (HRW, weight-friendly) are the two refinements you should know.
Quorum intuition
With N replicas, R read replicas, W write replicas: if R + W > N, every read set and every write set share at least one node (pigeonhole). So a read will see at least one replica that has the latest write. Typical balanced choice: R = W = ⌈N/2⌉ + 1.
Consistent hashing
Hash both keys and nodes onto a ring (e.g., 0..2^32). Each key is owned by the next clockwise node. Adding or removing a node only moves K/N keys, vs nearly all under naive modulo hashing. Virtual nodes (each physical node owns many ring positions) smooth out load when N is small.
Two refinements you should know
- Jump hash (Lamping & Veach 2014) — O(log n) compute, no node IDs needed, no memory state. Limitation: only works for "node count goes from N to N+1" — you can't remove arbitrary nodes mid-ring. Good for stateless services with a known cluster size.
- Rendezvous (HRW) hashing — for each key, hash
(key, node_id)for every node; pick the node with the max hash. Same K/N rebalance property as consistent hashing. Trivially supports weighted nodes (multiply hash by weight).
R + W > N guarantee — your read might land on the strict replicas while the write went to backup nodes. Don't claim strong consistency on a sloppy-quorum system.
- R + W > N ⇒ every read intersects every write.
- Consistent hashing: K/N keys move on add/remove. Virtual nodes for small N.
- Jump hash: O(log n), stateless, but rigid. HRW: same property + weights.
- Sloppy quorum ≠ quorum. Don't conflate.
Four topologies: single-leader (Postgres, MySQL — simple, write throughput bounded by leader), multi-leader (Cassandra-EACH_QUORUM, multi-region — needs conflict resolution), leaderless (Dynamo — sloppy quorums, gossip), chain replication (head→tail — strong consistency, simple recovery, used in object stores like Microsoft Azure FCRC). Sync vs async replication is the fundamental availability/durability dial.
The four topologies
- Single-leader: writes go to the leader, replicate to followers. Sync replication = no data loss but writes block on slowest follower; async = fast but you can lose recently-acked writes on failover. Most production setups use one sync follower (durability) and N async (read scaling).
- Multi-leader: writes can go to any leader; conflicts must be resolved (LWW, CRDTs, or app logic). Useful for multi-datacenter writes when you cannot tolerate cross-region write latency.
- Leaderless (Dynamo-style): any replica accepts writes; gossip + sloppy quorum + hinted handoff for availability; read-repair on conflict; anti-entropy via Merkle tree comparison.
- Chain replication (van Renesse & Schneider 2004): writes flow head → ... → tail; reads from tail. Strong consistency with simple failure recovery (just shorten/extend the chain).
Sync vs async — the durability dial
Sync replication: leader waits for followers to ack before returning OK to the client. No data loss on leader crash, but availability drops if a follower lags. Async: leader returns OK after local write; recently-acked writes can be lost if the leader dies before replication catches up. Semi-sync (MySQL, Postgres): wait for at least one follower to ack — usually the right tradeoff.
- Single-leader is the default; sync to one follower, async to the rest.
- Multi-leader needs conflict resolution. Don't use without one.
- Leaderless = Dynamo. Tunable consistency via R, W.
- Chain replication: strong consistency, simple recovery — underrated.
CRDTs are data structures whose merge is commutative, associative, and idempotent — replicas converge no matter the order of updates. G-Counter (sum of per-replica counters), OR-Set (tagged adds, tombstone-aware deletes), LWW-Register (timestamp wins), RGA (collaborative text). Used in Figma, Linear, Riak, and offline-first apps. The price: weaker semantics than serializable, more storage (tombstones).
The four you should know cold
- G-Counter (grow-only) — array of per-replica counters; increment your own slot; read = sum; merge = element-wise max.
- PN-Counter — two G-Counters (one for increments, one for decrements); read = inc - dec.
- OR-Set (observed-remove) — every add tags the element with a unique ID; remove only erases tags you have actually observed. Concurrent add+remove resolves to "present."
- LWW-Register — timestamped value; latest timestamp wins. Loses concurrent writes — use only when "last write wins" is semantically OK.
- RGA (replicated growable array) — text editing CRDT; each character has a unique ID and reference to its predecessor; insertion is commutative.
CRDT vs OT
Operational Transform (Google Docs) achieves the same goal differently: transform concurrent ops to commute. Requires a server to linearize. CRDTs converge peer-to-peer without one. OT is dominant historically; CRDTs dominate new collaborative apps because they're easier to reason about offline.
- CRDT = merge is commutative + associative + idempotent ⇒ converges.
- G-Counter, OR-Set, LWW-Register, RGA — name and explain each.
- CRDTs trade serializability for coordination-free availability.
You can never tell a slow node from a dead one (FLP again). The job of a failure detector is to bound the wrongness. Heartbeats give a binary alive/dead signal; phi-accrual gives a continuous suspicion level adapting to network jitter; SWIM is gossip-based for thousands of nodes.
The three you should know
- Heartbeats — periodic ping with timeout. Simple, but the timeout choice is brutal: too short and you false-positive; too long and you take forever to detect a real failure.
- Phi-accrual (Hayashibara 2004) — instead of binary, output a continuous suspicion level
φ = -log₁₀(probability of mistake). App tunes a threshold. Adapts to observed inter-arrival distribution. Used in Cassandra, Akka. - SWIM (Das 2002) — Scalable Weakly-consistent Infection-style membership. Each node periodically pings a random peer; on failure, asks K random peers to indirectly probe. Gossips suspicions and confirmations. Scales to thousands of nodes. Used in HashiCorp Serf, Memberlist, Consul.
- Heartbeat = binary, simple, hard to tune.
- Phi-accrual = continuous, adaptive, app picks threshold. Cassandra default.
- SWIM = gossip-based, scales to thousands of nodes.
Two storage philosophies: LSM trees (RocksDB, Cassandra, BigTable) optimize writes by appending to a log and merging in the background — fast writes, slower reads, needs compaction. B-trees (InnoDB, Postgres, BoltDB) update in place — fast reads, slower random writes, no compaction. LSM dominates write-heavy KV stores; B-tree dominates OLTP.
How LSM works in 4 lines
- Writes go to a memtable (sorted in-memory) + a write-ahead log on disk.
- When the memtable fills, it flushes to an SSTable (sorted, immutable file) at level 0.
- Background compaction merges SSTables across levels (size-tiered or leveled).
- Reads: check memtable, then each level; bloom filters skip levels that can't contain the key.
LSM tree
- Writes: fast (sequential append).
- Reads: slower (multi-level + bloom).
- Space amp: higher (multiple levels, tombstones).
- Compaction: required, background.
- Used by: RocksDB, Cassandra, BigTable, ScyllaDB, LevelDB.
B-tree
- Writes: slower (random IO).
- Reads: fast (single tree walk).
- Space amp: lower (in-place updates).
- Compaction: not needed; vacuum / page splits.
- Used by: InnoDB, Postgres, BoltDB.
- LSM = write-fast, read-amp, compaction. Dominates write-heavy KVs.
- B-tree = read-fast, in-place. Dominates OLTP.
- Both use bloom filters / page caches; the difference is what's on disk.
Six systems define the field. BigTable taught us range-partitioned KV. Dynamo taught us AP + tunable consistency. Spanner taught us global linearizability via TrueTime. Kafka taught us the log as primary abstraction. S3 taught us strong consistency at exabyte scale. CockroachDB taught us Spanner without atomic clocks. Know the one-line "killer move" of each.
| System | Killer move | Read this |
|---|---|---|
| BigTable (Chang 2006) | GFS + Chubby + tablet servers; sparse multi-dim sorted map. | Original BigTable paper. |
| Spanner (Corbett 2012) | Global linearizability via TrueTime + Paxos per shard + 2PC across shards. | Spanner paper, then CockroachDB design doc. |
| Dynamo (DeCandia 2007) | Consistent hashing + sloppy quorum + vector clocks. AP first. | Dynamo paper. Influenced Cassandra, Riak, DynamoDB. |
| Cassandra | Dynamo + BigTable hybrid. Tunable consistency (LOCAL_QUORUM, EACH_QUORUM). | Cassandra docs on consistency levels. |
| CockroachDB | Spanner without atomic clocks: HLC + max offset + Raft per range. | Cockroach blog, especially "Living Without Atomic Clocks." |
| Kafka (Kreps 2011) | Log-structured commit log; topic = partitioned log; ISR semantics. | LinkedIn paper + Confluent's exactly-once blog. |
| S3 | Strong read-after-write consistency since Dec 2020. Throughput sharded by prefix. | AWS strong consistency announcement; Marc Brooker's blog. |
Kafka exactly-once — the recipe
(1) Idempotent producer: producer ID + per-partition sequence number → broker dedups retried writes. (2) Transactions: producer wraps writes across partitions; transaction coordinator atomically commits or aborts. Consumers in read_committed isolation only see committed records.
Spanner: why both Paxos AND 2PC
Common confusion: "Spanner has Paxos, why does it need 2PC?" Paxos replicates within a single shard (Paxos group). Cross-shard transactions need 2PC across Paxos groups, where each "participant" is itself a fault-tolerant Paxos group. So 2PC's blocking problem is mitigated — the coordinator and every participant are themselves replicated.
- BigTable: range-partitioned KV on GFS+Chubby.
- Spanner: TrueTime + Paxos shards + 2PC across shards = strict serializable globally.
- Dynamo: AP + consistent hashing + sloppy quorum + vector clocks.
- Kafka: log = primary abstraction; exactly-once = idempotent producer + transactions.
- S3: strong R-A-W since Dec 2020; shard throughput by prefix.
The "back-of-envelope" service-design vocabulary: L4/L7 load balancing with P2C, cache hierarchies (cache-aside vs write-through), token-bucket rate limiting in Redis with Lua, fencing tokens for distributed locks, idempotency keys (Stripe), circuit breakers + bulkheads + jittered retries. Every system-design loop draws on these.
Load balancing
L4 = TCP-level (HAProxy in TCP mode, AWS NLB). L7 = HTTP-level (Envoy, NGINX, AWS ALB) — can route on path/header/cookie, terminate TLS, do retries. Algorithms: round-robin, least-connections, consistent-hash, EWMA.
Power of two choices (P2C) — pick 2 backends at random, send to the less loaded. Mitzenmacher's result: with O(N log log N / log N) max load vs O(log N / log log N) for naive random. Nearly optimal at almost zero coordination cost. Use it.
Caching
- Cache-aside (lazy): app reads cache; on miss, reads DB and populates cache.
- Write-through: writes hit cache and DB synchronously.
- Write-behind: writes hit cache; flushed to DB async. Risk: data loss on cache crash.
- Eviction policies: LRU (most common), LFU, ARC, TinyLFU, S3-FIFO, SIEVE. SIEVE (2024) is the new low-overhead default.
Rate limiting
Three classic shapes:
- Token bucket — steady refill rate, allows bursts up to bucket size. Most flexible.
- Leaky bucket — steady output rate, no burst. Use when downstream can't burst.
- Sliding window — count requests in last T seconds. Approximate variants for memory.
Atomic refill + deduct in a single round-trip. Tokens and last-refill timestamp are stored per key; the Lua script computes elapsed time, refills, and deducts in one shot.
-- KEYS[1] = bucket key; ARGV: rate, capacity, now
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens, ts = tonumber(b[1]), tonumber(b[2])
local rate, cap, now = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
if not tokens then tokens, ts = cap, now end
tokens = math.min(cap, tokens + (now - ts) * rate)
local allowed = 0
if tokens >= 1 then tokens = tokens - 1; allowed = 1 end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], 3600)
return allowed
For 100k+ req/s: shard Redis by key, or use approximate local counters with periodic sync to a central store. Stripe's pattern.
Other essentials
- Idempotency keys — client sends a UUID; server stores (key → result); safe retries. Stripe's pattern.
- Circuit breakers / bulkheads / timeouts / jittered exponential retries — Hystrix-style. Avoid retry storms with full jitter (random in [0, backoff]).
- Service mesh — Envoy / Istio sidecar proxies handle mTLS, retries, observability.
- Event sourcing — state = log of events. CQRS = separate read/write models.
- P2C beats round-robin almost everywhere with no coordination cost.
- Cache-aside is the default. Write-through for read-after-write.
- Token bucket in Redis with Lua = standard rate limiter; shard for >100k qps.
- Distributed locks need fencing tokens. Always.
- Retries: full jitter exponential backoff. Idempotency keys for safety.
TCP is reliable, ordered, has slow-start and HOL blocking. QUIC is UDP + TLS + multiplexed streams with no HOL between streams; HTTP/3 runs on it. HTTP/2 multiplexes on TCP (still HOL on packet loss); HTTP/3 fixes that on QUIC. gRPC = HTTP/2 + Protobuf, with streaming RPC and deadlines. TLS 1.3 is 1-RTT.
| Protocol | What it is | Why you'd use it |
|---|---|---|
| TCP | Stream, reliable, ordered. Slow-start, congestion control (CUBIC, BBR). HOL blocking on loss. | The default for everything reliable. |
| UDP | Datagram, unreliable, fast. No connection state. | QUIC's transport, DNS, video, games. |
| QUIC | UDP + TLS + multiplexed streams. No HOL between streams. 0-1 RTT handshake. | HTTP/3, faster mobile, packet-loss-tolerant. |
| HTTP/1.1 | One request per connection (pipelined rarely works). | Legacy; still simple to debug. |
| HTTP/2 | Multiplexed on TCP. HOL on packet loss (TCP, not stream). | Reduces connection overhead. |
| HTTP/3 | Multiplexed on QUIC. No HOL. | Mobile / lossy networks; Cloudflare default. |
| gRPC | HTTP/2 + Protobuf. Streaming RPC, deadlines, interceptors. | Internal RPC at Google, Anthropic, etc. |
| WebSocket | Long-lived bidirectional TCP connection. | Real-time push (chat, collab). |
| SSE | One-way server-to-client streaming over HTTP. | Token streaming from LLM APIs. |
| TLS 1.3 | 1-RTT handshake; 0-RTT for resumption (replay risk). | Default for everything HTTPS. |
- TCP HOL is at the transport layer; HTTP/2 doesn't fix it. HTTP/3 (QUIC) does.
- gRPC = HTTP/2 + Protobuf; bidirectional streaming.
- SSE for LLM token streaming; WebSocket for full bidi.
ML-specific distributed (covered elsewhere)
Parallel training and inference patterns live on distributed training and LLM inference. Vector DB internals are in ML system design problem 8.
- Raft (Ongaro & Ousterhout 2014) — read in full
- Spanner (Corbett 2012)
- Dynamo (DeCandia 2007)
- BigTable (Chang 2006)
- GFS (Ghemawat 2003)
- MapReduce (Dean 2004)
- ZooKeeper (Hunt 2010)
- Kafka (Kreps 2011) — LinkedIn paper
- Calvin (Thomson 2012) — deterministic distributed DB
- FLP impossibility (Fischer, Lynch, Paterson 1985)
0 → hero distributed systems path (the common weak area — invest heavily)
- foundation Designing Data-Intensive Applications (Kleppmann) — read all 12 chapters; 5–9 are core
- foundation Martin Kleppmann's Distributed Systems lecture series (Cambridge, free)
- foundation ByteByteGo — system design illustrations
- build MIT 6.824 Distributed Systems — implement Raft (Lab 2) and KVStore (Lab 3) in Go. The single most useful exercise.
- build Read etcd's Raft implementation
- depth Raft (Ongaro & Ousterhout 2014) — read in full
- depth Spanner (Corbett 2012)
- depth Dynamo (DeCandia 2007)
- depth BigTable (Chang 2006)
- depth GFS (Ghemawat 2003)
- depth MapReduce (Dean 2004)
- depth Martin Kleppmann's blog
- depth Marc Brooker's blog (AWS Principal Engineer — distributed systems essays)
- depth Murat Demirbas — paper reviews
- depth Jepsen — distributed systems testing reports (consistency violations in real systems)
- depth Companion: full ~104KB curriculum at
../distributed_systems_curriculum.md
Distributed systems quiz — readiness check
- Explain CAP precisely.
Show answer
Under network partition, you choose between consistency (linearizability) and availability (every non-failed node responds). P is not optional in real networks — you choose CP or AP during a partition. Common misreading: "pick 2 of 3" — wrong, because real systems must tolerate partitions.
- Difference between snapshot isolation and serializable?
Show answer
SI doesn't prevent write skew: two transactions both reading and updating disjoint rows based on a constraint that depends on the other's reads. SSI (Cahill 2008) adds predicate locks + dependency tracking to detect write skew. PostgreSQL's "serializable" is SSI.
- How does Spanner achieve external consistency?
Show answer
TrueTime gives bounded uncertainty intervals via GPS + atomic clocks. Commit-wait: after assigning a commit timestamp, wait for the uncertainty interval to pass before releasing the commit. Ensures any subsequent transaction sees this commit's timestamp.
- Walk through Raft.
Show answer
Three sub-problems: (1) Leader election: random timeouts → become candidate → request votes → win majority → leader. (2) Log replication: leader appends + replicates + commits when majority ack. (3) Safety: leader's log wins; only commit current-term entries. Membership changes via joint consensus.
- How does Dynamo handle conflicts?
Show answer
Vector clocks identify concurrent versions; client (or LWW) resolves on read. Sloppy quorum + hinted handoff for availability. Read repair: when read sees divergent versions, async write the merged value back. Anti-entropy via Merkle tree comparison between replicas.
- Compare LSM tree vs B-tree.
Show answer
LSM: write-fast (sequential append), read-amp (bloom + multi-level lookup), needs compaction. B-tree: balanced, in-place updates, slower writes. LSM dominates write-heavy KVs (Cassandra, RocksDB). B-tree dominates OLTP (Postgres, InnoDB).
- Design a rate limiter at 100k req/s per key.
Show answer
Token bucket per key in Redis with Lua atomic update: store tokens + last-refill timestamp; on each request, refill based on (now − last) × rate, capped; deduct if enough; return decision. Higher scale: shard Redis or local approximate counters with periodic sync.
- Kafka exactly-once semantics — how?
Show answer
(1) Idempotent producer: PID + per-partition sequence number → broker dedups retries. (2) Transactions: producer wraps writes across partitions in a transaction; transaction coordinator atomically commits/aborts. Consumers in read_committed only see committed messages.
- Consistent hashing vs jump hash vs rendezvous?
Show answer
Consistent hash: ring with virtual nodes; adding a node moves K/N keys. Jump hash (Lamping & Veach 2014): O(log n) compute, no node IDs. Rendezvous (HRW): hash (key, node) for all nodes; pick max — handles weighted nodes naturally.
- What is FLP and what does it mean for production systems?
Show answer
FLP impossibility (Fischer, Lynch, Paterson 1985): in a fully asynchronous network with even one crash, no deterministic protocol guarantees both safety and liveness. Real systems sidestep with timeouts (semi-synchrony assumption), randomization, or accept rare unavailability. Raft and Paxos prefer safety over liveness.
- Why use vector clocks vs Lamport clocks?
Show answer
Lamport clock: total order, doesn't preserve causality fully. Vector clock: per-process counter array; captures full causality (a → b iff VC(a) < VC(b)). Cost: size O(N processes). Use vector when you need to detect concurrent vs causally-ordered events; Lamport when you only need total order.
- Quorum math: R + W > N — why?
Show answer
If R + W > N, every read intersects every write (pigeonhole). So a read sees at least one replica that has the latest write. Strong-ish consistency without coordination. R = W = ⌈N/2⌉+1 is the typical balanced choice.
- Explain CRDTs and give two examples.
Show answer
Conflict-free replicated data types: structures that converge under concurrent updates without coordination. G-Counter: per-replica counter, sum on read. OR-Set: tag adds with unique IDs; deletes only remove tags you've seen. Used in collaborative editors (Figma, Linear) and offline-first apps.
- Why does Spanner need 2PC despite Paxos?
Show answer
Paxos is for replication within a Paxos group (one shard). Cross-shard transactions need 2PC across groups. Spanner: 2PC over Paxos — each "participant" in 2PC is a Paxos group, so the transaction is durable even with single-node failures.
- What is sloppy quorum + hinted handoff?
Show answer
If a designated replica is down, the write goes to the next node in the ring (sloppy — outside the strict quorum). The receiving node holds the data as a "hint" and forwards to the original when it recovers. Tradeoff: improves write availability; risks stale reads on the original.
- Phi-accrual vs heartbeat for failure detection?
Show answer
Heartbeat: ping with timeout; binary alive/dead. Phi-accrual (Hayashibara 2004): probabilistic — outputs a continuous suspicion level φ; threshold tuning per-app. Adapts to network jitter. Used in Cassandra, Akka.
- Design a distributed lock service.
Show answer
Cluster of 5 nodes running Raft. Lock = key in replicated log with TTL + owner. Acquire: CAS (set if not exists); on fail, watch for delete event. Release: delete key. Crash safety: TTL releases abandoned locks. Use fencing tokens (monotonic) to prevent stale clients from acting after lease expiry.
- Explain MapReduce in one paragraph.
Show answer
Distributed computation framework. User writes map(k, v) → list[(k, v)] and reduce(k, list[v]) → output. System partitions input across mappers, shuffles by key, runs reducers on grouped data. Handles fault tolerance (re-run failed tasks), data locality (run map close to data), straggler mitigation (speculative execution).
- What is read-your-writes consistency?
Show answer
A user always reads their own latest writes (even in an eventually-consistent system). Implemented by routing reads to the same replica that handled writes, or by tracking client write timestamps and reading from a replica caught up past that timestamp.
- What's PACELC and how does it extend CAP?
Show answer
PACELC (Abadi 2012): partitioned → choose C or A; else (no partition) → L (latency) or C. Most real systems trade off in normal operation too (not just during partitions). Spanner: CP / EC. Dynamo: AP / EL.