SYSTEMS PILLAR · STATED WEAK AREA · INVEST MOST

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.

Read ~45 min Asked at Anthropic, OpenAI, Google, Stripe, AWS, every infra company Difficulty Sr / Staff bar Companion ../distributed_systems_curriculum.md (~104KB)
01
FOUNDATIONS · THE LENS

CAP & PACELC — the lens that organizes everything

🎯Every distributed database is making exactly two bets: one during a partition, one during normal ops — CAP and PACELC name them.

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.

TL;DR

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.

CAP, stated correctly

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 "Pick 2 of 3" myth

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.

PACELC — the missing 99%

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:

P → A or C
During a partition: sacrifice Availability or Consistency. Same as CAP.
E → L or C
Else (normal operation): trade Latency or Consistency. This is the new insight.

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.

SystemPartition choiceElse choiceMechanism
SpannerCPECTrueTime commit-wait; Paxos per shard.
Dynamo / CassandraAPELSloppy quorum; tunable R/W.
HBase / BigTableCPECSingle tablet server per range; reads block on lease handoff.
MongoDB (default)CP-ishECPrimary-only writes; reads can relax via readPreference.
CockroachDBCPECHLC + max-clock-offset; Raft per range.
📐 If you get this question — the rule

Trigger: "Walk me through CAP theorem" or "Where does [database] sit on CAP?"

  1. Correct the framing first: P is not optional. The real choice is CP vs AP during a partition.
  2. Then immediately extend to PACELC: "But CAP only covers partition time. PACELC also asks what they trade in normal operation — latency vs consistency."
  3. 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)."
  4. 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.

⚠ Clears up: "CA systems exist, like a single-node database"

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.

⚠ Clears up: "Strong consistency = linearizability?"

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

✓ Remember
  • 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.
Tricky interview questions — chapter 01
Q1. A recruiter says "We need a CA database." What do you tell them?
Gently explain that CA is not achievable in a multi-node distributed system with a real network. Any two-node system can experience a partition, forcing either C or A to be sacrificed. A single-node system avoids the trade-off, but it cannot scale or tolerate node failure. The real question to ask: "During a partition, do you need every node to keep answering (AP), or do you need answers to be correct (CP)?" That re-frames the conversation productively.
Q2. You're designing a global payments system. Where on CAP/PACELC do you sit, and why?
Payments require correctness over availability: you cannot double-charge or lose a payment due to stale state. That means CP: during a partition, refuse to process rather than risk inconsistency. In normal ops, EC (consistency over latency) — a few milliseconds of extra latency is acceptable; a wrong balance is not. This is why Stripe uses Paxos-backed storage and why Spanner (CP/EC) is popular for financial workloads.
Q3. Cassandra's documentation claims "tunable consistency." Does that mean it can be CP?
In a narrow sense yes: if you set W + R > N (e.g., N=3, W=3, R=2), you are reading from a strict quorum and Cassandra behaves like CP for that operation. But Cassandra's architecture is fundamentally AP — it uses sloppy quorums and hinted handoff, meaning the R+W>N guarantee can break if the write went to substitute nodes. So "Cassandra tuned to CP" is more fragile than a system architected as CP from the start.
Q4. Explain PACELC with a real numbers example for Dynamo.
N=3 replicas, W=2, R=1. A client writes x=99; the write acks after 2 replicas confirm (fast — EL). The third replica may lag 50ms. Another client immediately reads with R=1 and lands on the lagging replica — gets x=42 (old value). No partition occurred. The system chose EL: it returned fast but sacrificed full consistency in steady state. To get EC, you'd need R=2 so the read quorum intersects the write quorum, but that adds a round-trip.
Q5. Why does Spanner call its guarantee "external consistency" rather than "linearizability"?
Linearizability is defined for single-object operations. Spanner provides external consistency across transactions: if transaction T1 commits before T2 starts (in wall-clock time), T2 must see T1's writes — even across datacenters. It's strictly stronger than single-object linearizability and matches what clients observe when they check their watches. The term "external consistency" comes from Lamport 1979 and is the transactional generalization of linearizability.
Q6. How does CockroachDB achieve CP/EC without GPS-based atomic clocks?
CockroachDB uses a Hybrid Logical Clock (HLC): a physical component anchored to NTP + a logical component for tie-breaking. It enforces a max-clock-offset (default 500ms). On a Raft leader lease, reads are served only if the lease is still valid (checked against HLC). Transactions that might violate causality are delayed until the offset window passes — similar to Spanner's commit-wait but bounded by NTP offset rather than TrueTime uncertainty. The trade-off: higher clock offset = longer potential delays; Spanner's GPS clocks reduce this to ~7ms.
Q7. "BigTable is CP — why does a tablet server failure cause brief unavailability?"
BigTable has exactly one tablet server per key range (no multi-master). When that server dies, the master must detect the failure (via Chubby lease expiry, ~10s default), re-assign the tablet, and the new server must replay the commit log before serving. During this window — typically 60–90s — the affected key range is unavailable. This is the CP tradeoff: BigTable halts during leader failure rather than serve stale data from a secondary. Bigtable is CP/EC on the PACELC matrix.
Q8. What breaks if you ignore PACELC and only think about CAP?
You under-count the consistency cost in normal operation. For example, you might choose Cassandra (AP per CAP) thinking you only sacrifice consistency during rare partitions — but in practice, with R=1 default reads, you're reading stale data on every operation even with perfect network health. PACELC forces you to design explicitly for the steady-state trade-off, which is what 99%+ of your real traffic hits.
02
FOUNDATIONS · IMPOSSIBILITY

The impossibility result — FLP and what it forced us to do

🎯FLP doesn't say consensus is impossible — it says you can't guarantee it finishes in a fully asynchronous world; every real protocol escapes by sneaking in a time assumption.

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.

TL;DR

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.

What "fully asynchronous" actually means

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 FLP proof idea — why one crash breaks everything

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.

The three escape hatches real systems use
  1. 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.
  2. 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.
  3. 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.
⚠ Clears up: "FLP means Raft/Paxos can deadlock"

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.

⚠ Clears up: "Consensus with failure detectors avoids FLP"

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.

📐 If you get this question — the rule

Trigger: "What is FLP impossibility?" or "Why can't you guarantee consensus in distributed systems?"

  1. State the result precisely: async + 1 crash + deterministic = no protocol guarantees both safety and liveness.
  2. Explain the model: "fully asynchronous" means no message-delay bounds, so you can't distinguish crash from slow.
  3. Name the three escapes: timeouts (semi-synchrony), randomization, or sacrifice liveness.
  4. 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.

✓ Remember
  • 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.
Tricky interview questions — chapter 02
Q1. What does FLP actually prove, and what does it not prove?
FLP proves that in a fully asynchronous distributed system where at least one process can fail by crashing (silently stopping), no deterministic consensus protocol can guarantee both safety (all correct processes decide the same value) and liveness (all correct processes eventually decide). It does NOT prove that consensus is impossible in practice — only that the combination of full asynchrony + crash failures + determinism makes termination impossible to guarantee. Real systems add timing assumptions (semi-synchrony) which FLP does not cover.
Q2. Raft uses random timeouts. How does this relate to FLP?
FLP applies to deterministic protocols. Raft's randomized election timeouts make it non-deterministic, which sidesteps the FLP model. Even so, Raft uses the semi-synchrony escape more fundamentally: it assumes messages arrive within some bound in practice. The randomized timeouts solve a specific problem — preventing split elections where two candidates tie — rather than defeating FLP directly. In an adversarial fully-async network, even randomized Raft could stall; in practice it converges within a few hundred milliseconds.
Q3. What is the difference between safety and liveness, and why does FLP force you to sacrifice one?
Safety: "nothing bad happens" — the system never decides two different values (no split-brain). Liveness: "something good eventually happens" — the system eventually decides a value and doesn't run forever without output. FLP shows that in a fully async system with one possible crash, you can always construct an execution that runs forever without deciding (no liveness violation yet) but also without violating safety. The adversary keeps the system in a bivalent state. You're forced to either add assumptions (semi-synchrony) or accept that liveness can be delayed under adversarial conditions.
Q4. What is an "eventually perfect failure detector" and how does it relate to consensus?
Chandra & Toueg (1996) showed that a consensus algorithm exists if and only if you have an eventually perfect failure detector (◇P): a failure detector that may wrongly suspect live nodes initially but eventually correctly identifies all crashed nodes and never wrongly suspects live ones. This characterizes exactly how much synchrony you need. Raft's timeouts approximate ◇P in practice — they may falsely trigger elections (wrongly suspect the leader) but eventually the stable leader is recognized. The "eventually" is the key concession to FLP: you're allowed to be wrong during the chaotic early period.
Q5. Can a Byzantine fault-tolerant protocol avoid FLP?
BFT protocols face an even harder version of FLP. With crash failures, you need f < n/2 for consensus; with Byzantine failures (arbitrary behavior including lying), you need f < n/3. FLP still applies to deterministic BFT protocols. Practical BFT (PBFT, HotStuff) also use semi-synchrony. The extra third of nodes exists to out-vote the Byzantine nodes — not to escape FLP. Randomized BFT protocols (like those used in some blockchains) use coin flips for the same reason Raft uses random timeouts: to escape determinism.
Q6. Raft halts during a split-brain. Walk through exactly what happens.
Say you have 5 Raft nodes and the network splits into partitions of 3 and 2. The partition of 3 can still elect a leader (3 > 5/2) and commit entries. The partition of 2 cannot — they can start an election but never win a majority. Any candidate in the 2-partition sends RequestVote but only gets 2 votes (including itself), which is not a majority of 5. So the 2-partition stalls: no new leader, no new commits. When the partition heals, the stalled nodes receive AppendEntries from the partition-3 leader with a higher term, update their logs, and resume. No safety violation occurred: the 2-partition never committed anything.
Q7. Why can you build a correct key-value store on top of Raft despite FLP?
Because FLP describes worst-case behavior under adversarial conditions, not typical behavior in a well-operated data center. Real networks deliver messages within milliseconds the vast majority of the time. Raft's election timeout (e.g., 150–300ms) means that even if a leader fails, a new one is elected within half a second. The "bad" FLP executions — where message delays are adversarially chosen to keep the system bivalent — don't occur organically. You're making an engineering bet that the environment is not fully adversarial, and that bet is almost always right in production.
Q8. What would a system that sacrifices safety look like, and why is this never acceptable?
A system that sacrifices safety to guarantee liveness would allow different nodes to commit different values in the same round — a split-brain. In a key-value store, this means two clients could both "successfully" write to the same key and get different values back. In a database, two nodes could both commit a transaction that transfers the same \$100 — doubling the spend. This is why virtually all consensus systems (Raft, Paxos, ZAB) prefer to halt over making a wrong decision. The only exception is optimistic systems like AP databases, which explicitly accept stale/conflicting reads and handle it at the application layer.
Q9. How does the Ben-Or randomized protocol escape FLP?
Ben-Or (1983) introduced coin flips into the decision process. In rounds where nodes disagree, instead of deterministically waiting for a majority, each node flips a coin to pick a value. The adversary cannot predict or control coin flips, so it cannot keep the system in a bivalent state forever — with some probability each round, the coin flips land the same way and consensus is reached. Ben-Or's protocol has expected polynomial termination. It's mostly of theoretical interest; practical systems use semi-synchrony instead because coin flips (especially distributed ones) are slow and complex.
03
FOUNDATIONS · CONSISTENCY MODELS

Consistency lattice — linearizable to eventual, with examples

🎯There are two completely separate consistency ladders — one for replication, one for transactions — and conflating them is the single most common mistake in distributed-systems interviews.

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.

TL;DR

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.

Replication consistency — strongest to weakest

These models describe what a single read operation can see on a replicated system, without assuming anything about transactions.

Linearizability
Definition: Every operation appears to take effect atomically at one instant between its invocation and response, consistent with real time. The system behaves as if there is a single copy of the data.
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).
Sequential consistency
Definition: All operations have a total order consistent with each process's own program order, but no real-time constraint.
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.
Causal consistency
Definition: Causally related operations are seen in the same order by all nodes. Concurrent (unrelated) operations may be seen in different orders.
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.
Eventual consistency
Definition: If updates stop, all replicas will converge to the same value — eventually. No bound on when.
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.
Transaction isolation — strongest to weakest

These models describe what a transaction can see of other concurrent transactions. They apply to databases with ACID-style transactions, not to standalone reads.

Serializable
The outcome is equivalent to some serial (one-at-a-time) execution of all transactions. No concurrency anomalies. The gold standard. Implemented via Two-Phase Locking (2PL) or Serializable Snapshot Isolation (SSI, Cahill 2008).
Snapshot Isolation (SI)
Each transaction reads from a consistent snapshot taken at its start time. Concurrent writes use first-writer-wins: if two transactions write the same row, the second one aborts. Blocks most anomalies — but NOT write skew (see below). Used by Oracle, PostgreSQL (before the "serializable" upgrade), MySQL REPEATABLE READ.
Read Committed
A transaction never reads uncommitted data. But within one transaction, two reads of the same row may return different values if another transaction commits between them (non-repeatable read). Default for most databases: PostgreSQL, Oracle, SQL Server.
Read Uncommitted
A transaction can read data that another transaction has written but not yet committed (dirty read). Rarely useful; can see values that later get rolled back. MySQL MyISAM; almost never correct in production.
The write skew anomaly under Snapshot Isolation

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 — the gold standard, and where Spanner sits

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

⚠ Clears up: "Linearizable" vs "Serializable" — they are NOT synonyms

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.

⚠ Clears up: "PostgreSQL serializable is not real serializability"

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.

📐 If you get this question — the rule

Trigger: "What consistency does your database give?" or "What isolation level would you use for [scenario]?"

  1. Distinguish the two ladders immediately: replication consistency (what can a read see?) and transaction isolation (what can a transaction see of concurrent transactions?).
  2. 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).
  3. 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.

✓ Remember
  • 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.
Tricky interview questions — chapter 03
Q1. What is write skew and why doesn't Snapshot Isolation catch it?
Write skew occurs when two concurrent transactions each read a shared condition, then write to disjoint rows in a way that collectively violates the condition. SI's conflict detection only catches writes to the same row (first-writer-wins). Since the transactions write to different rows, SI sees no conflict and commits both — violating the invariant. The fix is SSI (predicate locking tracks read dependencies) or explicit SELECT FOR UPDATE to serialize the reads. The on-call doctor example is the canonical illustration.
Q2. Explain the difference between linearizability and serializability with concrete examples.
Linearizability: single-operation, real-time ordering. If write x=5 completes at T=1, any read starting after T=1 must return 5. Serializability: multi-operation transactions, logical ordering. Two transactions T1 and T2 must produce an outcome equivalent to T1-then-T2 or T2-then-T1, but the serial order doesn't have to match wall-clock time. A system can be linearizable (fresh reads) but not serializable (write skew between transactions). Or serializable but not linearizable (the serial order is logically valid but out of wall-clock order — allowed in theory). Strict serializable combines both.
Q3. What consistency does Cassandra provide by default, and what anomalies can occur?
Default Cassandra (R=1, W=1, N=3) provides eventual consistency. Anomalies: (1) stale reads — a read may return a value overwritten hours ago if it lands on a lagging replica; (2) out-of-order reads — two consecutive reads of the same key can return x=5 then x=3 (reading from different replicas in different states). With LOCAL_QUORUM (R=2, W=2, N=3), you get quorum reads that mostly eliminate stale reads but Cassandra still lacks linearizability: there is no "last write wins by real time" guarantee across concurrent writes.
Q4. A colleague says "our system is ACID compliant, so we have no consistency problems." What's wrong?
The C in ACID means the database moves between valid states (no referential integrity violations, check constraints hold). It says nothing about what happens under concurrent access from a replication standpoint. An ACID-compliant system can still have stale replicas (eventual consistency on the read side), non-linearizable reads, or read-your-writes violations if you read from an async replica. Full correctness requires both ACID transactions AND a strong replication consistency model — which is why systems like Spanner are designed to combine SSI-style transactions with linearizable replication.
Q5. You're building an inventory system. Under Snapshot Isolation, can you safely decrement a counter?
Not safely if multiple transactions can decrement simultaneously. Example: inventory=3, two transactions both read 3 and each decrement by 1. Under SI (no same-row conflict here if they interleave right, though often they do conflict), both might commit as decrement-to-2 — losing one decrement. Even with first-writer-wins (one aborts), you need to be careful about the predicate "inventory >= 1." The safest pattern is SELECT ... FOR UPDATE on the inventory row, or use a serializable isolation level. Alternatively, use optimistic locking with a version column and retry on conflict.
Q6. What is "read-your-writes" consistency and when does it break in practice?
Read-your-writes (RYW) consistency means a client always reads its own most recent writes, even in an eventually consistent system. It breaks whenever a client can read from a replica that hasn't yet received its write. Example: User submits a profile update (hits server A), then immediately refreshes their page (hits server B, which hasn't replicated yet) — they see the old profile. RYW is implemented by routing reads to the same replica as writes, or by tagging writes with a timestamp and routing reads to replicas that have caught up to that timestamp. Social feeds often implement this via "sticky sessions" or client-side version tokens.
Q7. What is the causal consistency guarantee, and which real systems provide it?
Causal consistency ensures that causally-related operations are seen in the same order by all nodes. If A posts "check out my photo" (operation 1) and then posts the photo (operation 2), a node that sees operation 2 must have already seen operation 1. Causally independent operations (two users independently posting unrelated content) can appear in different orders on different nodes. Real systems: COPS (Facebook research, using version vectors to track causality), some Cassandra configurations with client-side causality tracking, and MongoDB's causally consistent sessions. It's stronger than eventual but weaker than linearizability — and requires no cross-replica coordination for reads.
Q8. What does SSI (Serializable Snapshot Isolation) add to plain Snapshot Isolation?
SSI (Cahill et al. 2008) adds predicate dependency tracking. It monitors read-write dependencies between transactions: if T1 reads a row that T2 later writes, and T2 reads something T1 later writes (a "dangerous" cycle in the dependency graph), SSI aborts one of the transactions. This detects write skew without requiring row-level locks on reads (unlike 2PL). The trade-off: SSI may abort transactions that 2PL wouldn't (false positives in the cycle detection), but it's much more concurrent than 2PL's pessimistic locking. PostgreSQL 9.1+ uses SSI for its "serializable" level.
04
FOUNDATIONS · TIME

Time in distributed systems — Lamport, vector, HLC, TrueTime

🎯You cannot trust wall-clock time across machines — instead, distributed systems either create logical order (Lamport/vector clocks) or bound the clock error and wait through it (TrueTime).

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

TL;DR

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.

Why wall clocks fail in distributed systems

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 at 998ms (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.

Lamport clocks — total order without causality

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.

$$L_{recv} = \max(L_{local},\; L_{msg}) + 1$$
L_recv: new local counter after receiving a message with timestamp L_msg; L_local: current local counter before receipt.

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.

Vector clocks — full causality detection

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.

$$VC_{recv}[i] = \max(VC_{local}[i],\; VC_{msg}[i]) \;\forall i;\quad VC_{recv}[self] \mathrel{+}= 1$$
For each process i, take the max of your local vector clock and the received vector clock. Then increment your own position.

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

Hybrid Logical Clocks (HLC) — the practical compromise

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

TrueTime — bound the uncertainty, then sleep through it

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:

  1. Transaction T1 wants to commit. Calls TT.now() → returns [10:00:00.000, 10:00:00.007]. Uncertainty: 7ms.
  2. Spanner assigns commit timestamp = latest = 10:00:00.007 (safe upper bound — definitely in the future).
  3. Spanner writes commit to the Paxos log.
  4. Commit-wait: Spanner sleeps until TT.now().earliest > 10:00:00.007. This takes approximately 7ms.
  5. 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 typeCausalityPhysical anchorSizeUsed by
NTP/PTPNoneYes (~10ms/<1ms)8 bytesLogging, metrics
LamportPartial (→ only)No8 bytesEvent ordering
Vector clockFull (→ and ∥)NoO(N) bytesDynamo, Riak
HLCPartial + bounded physicalYes (bounded ε)16 bytesCockroachDB, MongoDB
TrueTimeFull (external consistency)Yes (GPS, ~7ms)Interval [earliest,latest]Spanner
📐 If you get this question — the rule

Trigger: "How does Spanner achieve global consistency?" or "Why can't you just use wall-clock time?"

  1. Explain why wall clocks fail: NTP has ~10ms drift, clocks can go backward, so you can't use them for ordering across machines.
  2. Name the logical clock progression: Lamport (total order, no causality) → vector (full causality, O(N)) → HLC (practical compromise with physical anchor).
  3. For Spanner specifically: TrueTime gives a bounded uncertainty interval; commit-wait sleeps until the interval passes, guaranteeing external consistency without cross-datacenter coordination.
  4. 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.

✓ Remember
  • 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.
Tricky interview questions — chapter 04
Q1. Lamport says L(a) < L(b). Does this mean a happened-before b?
No. Lamport clocks give you "if a→b then L(a)<L(b)" but NOT the converse. Two concurrent events (no causal relationship) still get a total order from Lamport, but that order is artificial — it could be reversed with a different tie-breaking rule. To detect concurrency vs causality, you need vector clocks. This asymmetry is the fundamental limitation of Lamport clocks.
Q2. Vector clocks have O(N) size per message. How do real systems handle this at scale?
Dynamo's original paper used vector clocks but found they grew unbounded in some workloads. Riak replaced vector clocks with dotted version vectors (DVV) — a more compact representation that still detects concurrency. Another approach: use a causality-token scheme where only a subset of nodes contribute to the clock (as in CockroachDB's HLC — each node contributes one counter pair). At truly large scale (thousands of nodes), full vector clocks are impractical; systems either limit the nodes that write to a key, or abandon full causality tracking in favor of last-write-wins.
Q3. Walk me through Spanner's commit-wait with actual numbers.
Transaction T1 commits. TrueTime returns [1000ms, 1007ms] (7ms uncertainty). Spanner assigns commit_ts = 1007ms. It writes to the Paxos log (durable). Then it waits: it polls TrueTime until TT.now().earliest > 1007ms — typically ~7ms of sleep. It then releases the lock and makes the data visible. Any subsequent transaction T2 will see TT.now().earliest > 1007ms, so T2's timestamp is strictly > 1007ms. The commit history is thus externally consistent: T1's record precedes T2's in the committed order, matching real-world observation order.
Q4. How does CockroachDB achieve Spanner-like consistency without atomic clocks?
CockroachDB uses HLC (Hybrid Logical Clock) with a max-clock-offset bound (default 500ms). A read on a Raft leader is safe if the leader's HLC is ahead of the read timestamp by at least max-offset — similar to Spanner's commit-wait but bounded by NTP uncertainty (500ms) rather than TrueTime uncertainty (7ms). CockroachDB can wait longer per write transaction compared to Spanner, and if clocks skew beyond 500ms, nodes are forcibly removed. In practice, well-monitored clusters stay within 10–50ms of drift, making the wait negligible.
Q5. What is external consistency, and how does it differ from linearizability?
Linearizability applies to individual operations: each operation appears to happen at a single point in real time, and the order is consistent with observation. External consistency (Lamport 1979) applies to transactions: if transaction T1 commits before T2 starts in real time (a client finishes seeing T1 commit before starting T2), then the committed order must have T1 before T2. It's the transactional generalization of linearizability. Spanner provides external consistency across datacenters — a strictly stronger guarantee than per-object linearizability because it extends to multi-object, multi-shard transactions.
Q6. Why can't you use NTP + a large max-offset to approximate TrueTime cheaply?
You can, at a cost in latency. TrueTime's commit-wait duration equals the uncertainty interval. With NTP (±10ms uncertainty), commit-wait would be 10–20ms per write transaction. With Spanner's GPS+atomic clocks (~7ms), it's 7ms. With PTP in a controlled LAN (<1ms), you could get sub-millisecond. The trade-off is direct: tighter clock bounds = shorter commit-wait = lower write latency. CockroachDB using NTP with a 500ms max-offset would have to wait 500ms per write — unusable. It instead uses HLC with uncertainty typically <50ms in practice, making writes tolerable.
Q7. Two processes exchange a message. Draw the vector clocks and show which events are concurrent.
Setup: 2 processes, A and B. Events: a1 on A (VC=[1,0]), a1 sends to B, b1 on B (VC=[0,1]), B receives from A (VC=[max(0,1)+0, max(1,0)+1] = [1,2]). So a1 has VC=[1,0] and b1 has VC=[0,1]. Comparing element-wise: [1,0] vs [0,1] — neither is less than the other. So a1 and b1 are concurrent (no causal relationship). The message-receive event on B has VC=[1,2] — it is causally after both a1 and b1. This is the power of vector clocks: the comparison directly tells you whether events are causally related or concurrent.
Q8. In what situation would you use Lamport clocks over vector clocks?
Use Lamport when you only need a total order for log aggregation or event sequencing, and you don't need to detect whether events are causally related or concurrent. For example, merging debug logs from multiple servers where you just want a best-effort chronological order — Lamport gives a consistent total order at O(1) per message. Use vector clocks when you need to detect conflicts (like Dynamo detecting concurrent writes to the same key that need resolution) or provide causally consistent reads. The O(N) overhead of vector clocks is only worth it when concurrency detection is a correctness requirement.
05
PROTOCOLS · TRANSACTIONS

Distributed transactions — 2PC, sagas, when each works

TL;DR

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

  1. Coordinator sends PREPARE to all participants.
  2. Each participant locks resources, writes a prepare record to its log, votes YES or NO.
  3. If all YES, coordinator writes COMMIT to its log, sends COMMIT. If any NO or timeout, sends ABORT.
  4. 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.
REMEMBER
  • 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.
06
PROTOCOLS · CONSENSUS

Consensus — Paxos vs Raft vs ZAB

TL;DR

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.

THE INSIGHT — Raft in 3 rules

Memorize these and you can answer any Raft question

  1. 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.
  2. Replicate the log; commit by majority. Leader appends entries, sends AppendEntries to followers; once a majority has stored an entry, it is committed and applied to the state machine.
  3. 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.

REMEMBER
  • 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.
07
DATA PLACEMENT · QUORUMS

Quorums & consistent hashing — how data finds its replica

TL;DR

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

PITFALL — sloppy quorum is not strict quorum
Dynamo-style "sloppy quorum" sends writes to the next available nodes if the designated ones are down (with hinted handoff to forward later). It improves availability but breaks the 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.
REMEMBER
  • 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.
08
REPLICATION · TOPOLOGIES

Replication topologies — single-leader to leaderless

TL;DR

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

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.

REMEMBER
  • 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.
09
REPLICATION · CONFLICT-FREE

CRDTs — convergence without coordination

TL;DR

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

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.

REMEMBER
  • 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.
10
FAULTS · DETECTION

Failure detection — heartbeats, phi-accrual, SWIM

TL;DR

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

REMEMBER
  • Heartbeat = binary, simple, hard to tune.
  • Phi-accrual = continuous, adaptive, app picks threshold. Cassandra default.
  • SWIM = gossip-based, scales to thousands of nodes.
11
STORAGE · ENGINES

Storage engines — LSM vs B-tree, the canonical choice

TL;DR

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

  1. Writes go to a memtable (sorted in-memory) + a write-ahead log on disk.
  2. When the memtable fills, it flushes to an SSTable (sorted, immutable file) at level 0.
  3. Background compaction merges SSTables across levels (size-tiered or leveled).
  4. 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.
REMEMBER
  • 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.
12
CANONICAL SYSTEMS

The systems you must know — Spanner, Dynamo, BigTable, Kafka, S3

TL;DR

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.

SystemKiller moveRead 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.
CassandraDynamo + BigTable hybrid. Tunable consistency (LOCAL_QUORUM, EACH_QUORUM).Cassandra docs on consistency levels.
CockroachDBSpanner 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.
S3Strong 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.

REMEMBER
  • 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.
13
SERVICES · ARCHITECTURE

Service architecture — load balancing, caching, rate limiting

TL;DR

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

Rate limiting

Three classic shapes:

EXAMPLE — distributed token bucket in Redis Lua

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

PITFALL — distributed locks need fencing tokens
A client acquires a Redis/etcd lock with a 30s lease, then GCs for 40s. Lease expires; another client acquires the same lock. The first client wakes up and writes to the protected resource — corruption. Fix: every lock issues a monotonically increasing fencing token; the protected resource rejects writes with a stale token. (Martin Kleppmann's "How to do distributed locking" essay — must read.)
REMEMBER
  • 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.
14
NETWORKING · QUICK REFERENCE

Networking quick reference — TCP, QUIC, gRPC

TL;DR

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.

ProtocolWhat it isWhy you'd use it
TCPStream, reliable, ordered. Slow-start, congestion control (CUBIC, BBR). HOL blocking on loss.The default for everything reliable.
UDPDatagram, unreliable, fast. No connection state.QUIC's transport, DNS, video, games.
QUICUDP + TLS + multiplexed streams. No HOL between streams. 0-1 RTT handshake.HTTP/3, faster mobile, packet-loss-tolerant.
HTTP/1.1One request per connection (pipelined rarely works).Legacy; still simple to debug.
HTTP/2Multiplexed on TCP. HOL on packet loss (TCP, not stream).Reduces connection overhead.
HTTP/3Multiplexed on QUIC. No HOL.Mobile / lossy networks; Cloudflare default.
gRPCHTTP/2 + Protobuf. Streaming RPC, deadlines, interceptors.Internal RPC at Google, Anthropic, etc.
WebSocketLong-lived bidirectional TCP connection.Real-time push (chat, collab).
SSEOne-way server-to-client streaming over HTTP.Token streaming from LLM APIs.
TLS 1.31-RTT handshake; 0-RTT for resumption (replay risk).Default for everything HTTPS.
REMEMBER
  • 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.

CANON · 10 PAPERS

The 10 must-know papers

  1. Raft (Ongaro & Ousterhout 2014) — read in full
  2. Spanner (Corbett 2012)
  3. Dynamo (DeCandia 2007)
  4. BigTable (Chang 2006)
  5. GFS (Ghemawat 2003)
  6. MapReduce (Dean 2004)
  7. ZooKeeper (Hunt 2010)
  8. Kafka (Kreps 2011) — LinkedIn paper
  9. Calvin (Thomson 2012) — deterministic distributed DB
  10. FLP impossibility (Fischer, Lynch, Paterson 1985)

0 → hero distributed systems path (the common weak area — invest heavily)

  1. foundation Designing Data-Intensive Applications (Kleppmann) — read all 12 chapters; 5–9 are core
  2. foundation Martin Kleppmann's Distributed Systems lecture series (Cambridge, free)
  3. foundation ByteByteGo — system design illustrations
  4. build MIT 6.824 Distributed Systems — implement Raft (Lab 2) and KVStore (Lab 3) in Go. The single most useful exercise.
  5. build Read etcd's Raft implementation
  6. depth Raft (Ongaro & Ousterhout 2014) — read in full
  7. depth Spanner (Corbett 2012)
  8. depth Dynamo (DeCandia 2007)
  9. depth BigTable (Chang 2006)
  10. depth GFS (Ghemawat 2003)
  11. depth MapReduce (Dean 2004)
  12. depth Martin Kleppmann's blog
  13. depth Marc Brooker's blog (AWS Principal Engineer — distributed systems essays)
  14. depth Murat Demirbas — paper reviews
  15. depth Jepsen — distributed systems testing reports (consistency violations in real systems)
  16. depth Companion: full ~104KB curriculum at ../distributed_systems_curriculum.md

Distributed systems quiz — readiness check

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

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

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

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

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

  6. 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).

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

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

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

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

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

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

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

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

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

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

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

  18. 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).

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

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