Distributed systems theory
The capstone designs a system that spans many machines — and the moment state is replicated across nodes, a new body of theory applies. The fallacies, consensus (Raft, Paxos), quorums (R+W>N), commit protocols, logical clocks, and the consistency spectrum from linearizable to eventual — with runnable quorum and vector-clock models, closing the CAP story from SD6 with PACELC.
Learning objectives
- Recite the fallacies of distributed computing and why each bites.
- Explain consensus and how Raft elects a leader and replicates a log.
- Compute quorum requirements (R + W > N) and reason about the trade-off.
- Contrast 2-phase and 3-phase commit and order events with vector clocks.
- Place a system on the consistency spectrum and state CAP/PACELC.
30-second recap
- Scalability & CAP: Scale up (bigger machine) or out (more machines); replicas trade consistency for availability, and CAP says you can't have both during a partition.
1 · The fallacies of distributed computing
Here's a bug you'll write exactly once, and never forget. Your service calls another service — a quick HTTP request to fetch a user's profile. It works flawlessly for months. Then one afternoon the other service's box gets a little slow, the request never returns, and because you never set a timeout, your thread blocks forever. Threads pile up behind it, your pool exhausts, and a service that had nothing wrong with it falls over — because a call you assumed was instant and reliable was neither. That outage is not a coding mistake in the usual sense. It's a belief mistake: you quietly assumed the network behaves like a function call. It doesn't.
Here's the everyday analogy. Talking to another process on the same machine is like asking your roommate a question — instant, reliable, they're right there. Talking to a service across a network is like mailing a letter to another country: it might arrive late, arrive twice, arrive damaged, or never arrive at all, and you often can't tell which of those happened. The whole discipline of distributed systems is the habit of writing code that stays correct when the letter gets lost — and the fallacies are the catalogue of comforting lies that make you forget the letter can get lost.
The common mistake is treating these as trivia to memorise for an interview. They're not a list — they're a checklist of things that will page you at 3am if you ignore them. "The network is reliable" isn't a philosophical claim; it's the difference between code that retries and code that hangs. Read each fallacy as a question: "what breaks in my design if the opposite is true?" That reframing is the entire value of the list.
Peter Deutsch and colleagues catalogued the false assumptions engineers make when they first build across machines. Every one of them causes a real outage class. The network is not reliable, latency is not zero, bandwidth is not infinite, the network is not secure, topology changes, there is not one administrator, transport cost is not zero, and the network is not homogeneous. The discipline of distributed systems is essentially the art of not believing these.
Compare the two panels side by side: the left is the comforting mental model, the right is reality, split by the dashed line.
- The left panel is a same-machine call — a green round-trip that is instant, reliable, and returns exactly one answer. This is the model the fallacies quietly assume.
- The right panel is the same call across a network: the red arrow to
service Bmay be lost, delayed, or duplicated, and the amber box says the worst part — you often can't tell which failure happened. - The amber line at the bottom is the payoff: believe "the network is reliable" (skip the timeout) and the caller thread hangs forever, taking down a service that had nothing wrong with it.
In short: Each fallacy is one of these network failures you refused to plan for — the whole chapter is the code you write once you stop denying them.
| Fallacy | Reality | What it causes if believed |
|---|---|---|
| The network is reliable | packets drop; nodes partition | no retries/timeouts → hangs |
| Latency is zero | cross-region RTT is tens of ms | chatty calls → slow pages |
| Bandwidth is infinite | links saturate | huge payloads → congestion |
| Topology doesn't change | nodes come and go | hard-coded hosts → outages |
| Transport cost is zero | serialization + I/O cost real time | ignored overhead → surprise bills |
2 · Consensus — Raft & Paxos
Imagine three friends trying to agree on a restaurant over a group chat where messages randomly arrive late, out of order, or not at all — and one friend's phone keeps dying mid-conversation. How do you ever reach a decision everyone will honour, such that nobody shows up at the wrong place? That awkward little problem, scaled up and made rigorous, is consensus — and it turns out to be one of the deepest problems in computer science, because the messages really can be lost and the participants really can crash at the worst possible moment.
Why does this matter so much? Because the instant you have more than one copy of your data, you have a disagreement waiting to happen. Two nodes both think they're in charge; two writes arrive in different orders on different replicas; a node that was asleep wakes up with a stale view of the world. Consensus is the machinery that lets a group of unreliable machines behave, from the outside, like a single reliable one. The trick that makes it work is deceptively simple: a majority must agree. Because any two majorities of the same cluster must share at least one member (they overlap), a decision blessed by a majority can never be contradicted by a different majority — that overlap is the mathematical backbone of everything in this chapter.
The common mistake is thinking consensus means "everyone agrees." It doesn't — requiring everyone would mean a single dead node freezes the whole system, which is exactly the fragility we're trying to escape. Consensus asks only for a majority, so the cluster keeps making progress as long as more than half its nodes are alive and can talk. Another trap: people hear "Paxos is the hard one, Raft is the easy one" and assume Raft is weaker. It isn't — Raft gives the same safety guarantees; it just packages them as an explicit elected leader and a replicated log, which humans can actually reason about. Keep that framing — majority, not unanimity; understandable, not weaker — as you read the precise version below.
Consensus is getting a set of nodes to agree on a single value (or an ordered log of values) even though some may fail and messages may be lost. It is the foundation of replicated state machines — every node applies the same log in the same order, so they stay identical. Raft is the consensus algorithm designed to be understandable: it elects a single leader that accepts all writes, appends them to a log, and replicates them; an entry is committed once a majority has stored it. Paxos solves the same problem and came first, but its "prepare/promise/accept" phases are notoriously hard to follow — Raft trades nothing in guarantees for a lot in clarity.
Read it left-to-right as the life of a single log entry, following the blue arrows through the four stages.
- The "Leader" box (left) is where every write starts — the one elected node that accepts writes and appends each to its log.
- The middle boxes, "Append to majority" then "Committed" (green), are the safety step: the entry is replicated to more than half the cluster, and only once a majority has stored it does it become committed.
- The last box, "Apply" (amber), is the payoff — a committed entry is applied to each node's state machine, so every replica ends up in the same state in the same order.
In short: An entry flows leader → majority → committed → applied, which is how unreliable nodes stay a single consistent replicated log.
| Raft | Paxos | |
|---|---|---|
| Goal | agree on a replicated log | agree on a value (multi-Paxos: a log) |
| Leader | explicit, elected | implicit (proposer) |
| Reputation | designed for understandability | correct but hard to reason about |
| Used by | etcd, Consul, CockroachDB | Chubby, Spanner (variants) |
3 · Leader election
Think about how a group of people with no chairperson start a meeting. There's an awkward silence — everyone's waiting for someone else to begin. Eventually one impatient person can't stand it any longer, speaks up, and says "okay, I'll run this." If nobody objects and enough people nod along, they're now leading. But if two people pipe up at the exact same instant, they talk over each other, everyone gets confused, and the group has to reset and try again. Raft's leader election is precisely this — with a clever twist that makes the "two people at once" collision rare on purpose.
Why does the cluster need a leader at all? Because funnelling every write through one node is what makes the log simple to keep consistent — there's a single place that decides the order of events, so replicas never have to argue about who came first. The hard part is choosing that leader safely when the old one dies and nobody has a global clock or a referee. The mechanism rests on two ideas: a term (a monotonically increasing number that acts as a logical epoch, so everyone can tell a fresh leader from a stale one) and the majority vote from §2 (a candidate is only leader if more than half the cluster backed it in that term, which guarantees there can never be two legitimate leaders at once).
The genuinely clever bit — and the common point of confusion — is the randomised election timeout. If every follower waited the same fixed time before declaring itself a candidate, they'd all declare simultaneously, split the vote evenly, and nobody would win — a livelock that could repeat forever. So each node instead waits a random interval. Almost always one node's timer fires first, it becomes candidate before the others even wake up, and it wins cleanly. The occasional tie just triggers another round with fresh random timers, which almost certainly breaks it. The mistake to avoid: thinking randomness here is sloppy or non-deterministic hand-waving. It's the opposite — it's a deliberate, load-bearing technique for making a distributed race converge quickly.
Raft keeps time in terms (monotonic election epochs). Each node is a follower, candidate, or leader. If a follower hears nothing from a leader before a randomised election timeout, it becomes a candidate, increments the term, and requests votes. A node grants at most one vote per term; a candidate that collects a majority becomes leader. Randomised timeouts make simultaneous candidacies rare, so a split vote is unlikely and quickly resolved in the next term.
Follow the blue arrows left-to-right — they are the path a healthy node takes from waiting to leading.
- The Follower (left) is the resting state: it just waits to hear from a leader. When its randomised election timeout fires first, it advances to the middle.
- The Candidate (amber, middle) increments the term and asks everyone for votes. The green arrow shows the good outcome: collecting a majority promotes it to Leader (right), the single node that accepts all writes.
- The amber loop back to Candidate is the failure case — a split vote where no one reached a majority. It bumps the term and retries with fresh random timers, which almost always breaks the tie.
In short: Randomised timeouts pick one candidate at a time and a majority anoints it — so the cluster converges on exactly one leader per term.
election.py# MODEL: a candidate needs a strict majority of the cluster to become leader.
def majority(n):
return n // 2 + 1 # >50% of n nodes
def election(n_nodes, votes_for_candidate):
need = majority(n_nodes)
won = votes_for_candidate >= need
return {"cluster": n_nodes, "need": need,
"got": votes_for_candidate, "elected": won}
print(election(5, 3)) # 3 of 5 -> elected
print(election(5, 2)) # 2 of 5 -> not elected
print(election(4, 2)) # 2 of 4 is NOT a majority (need 3)
{'cluster': 5, 'need': 3, 'got': 3, 'elected': True}
{'cluster': 5, 'need': 3, 'got': 2, 'elected': False}
{'cluster': 4, 'need': 3, 'got': 2, 'elected': False}
4 · Replication & quorums (R + W > N)
Picture a study group that keeps five identical copies of the class notes — one with each member — so that if someone's laptop dies, the notes survive. Now a subtle question: when Priya updates the notes and then Sam reads them, how do you guarantee Sam sees Priya's edit and not an old copy? If Priya only bothered to sync her change to two friends, and Sam happens to read from the other three, he'll miss it entirely. The fix is beautifully simple once you see it: make the group Priya writes to and the group Sam reads from big enough that they're forced to share at least one person. That shared person always has the latest notes — and that single idea is the whole theory of quorums.
This is where the abstract "majority overlap" from consensus becomes a dial you actually turn. Every replicated store answers two questions per operation: how many replicas must acknowledge a write (call it W) and how many must answer a read (call it R), out of N total copies. If R + W > N, the read set and the write set cannot fit into N nodes without colliding — they must overlap — so a read is guaranteed to catch the newest write. That's strong consistency, bought with one inequality. If R + W ≤ N, there's room for them to miss each other, and a read can return stale data — but each operation talks to fewer nodes, so it's faster and survives more failures.
The mistake almost everyone makes at first is treating "strong consistency" as obviously better and just switching it on. But every replica you require to answer is a replica that can be down and block you. Demand acknowledgements from more nodes and your write gets slower and more fragile the moment any of them hiccups; demand fewer and you trade freshness for speed and resilience. There is no setting that wins on all axes — you're picking a point on a trade-off curve, and the honest engineer picks it per workload (a shopping cart tolerates staleness; a bank balance does not). Systems like Dynamo and Cassandra expose R and W directly precisely so you can make that choice operation by operation. Read the rule below as "how much overlap am I buying, and what availability am I paying for it?"
Data is replicated to N nodes. A write quorum W is how many must acknowledge a write; a read quorum R is how many must respond to a read. The key rule: if R + W > N, the read set and write set must overlap in at least one node, so a read is guaranteed to see the latest write — strong consistency. If R + W ≤ N, a read can miss the newest write — eventual consistency, but with lower latency and higher availability. Tuning R and W is how systems like Dynamo/Cassandra dial the CAP trade-off per operation.
Look at the same five replicas in two configurations, split by the dashed line, and ask: does the write set share any node with the read set?
- The row of boxes is the N=5 replicas. Green marks the write set W (nodes that acknowledged the write); amber marks the read set R (nodes the read consulted).
- On the left, W={n1,n2,n3} and R={n3,n4,n5} — because 3+3 > 5 they are forced to share
n3(the blue arrow). That shared node carries the latest value, so the read is guaranteed to see it. - On the right, W={n1} and R={n5} — 1+1 ≤ 5 leaves room for the sets to miss each other completely, so the read can return a stale value. The upside is each op touches fewer nodes: lower latency, more failures tolerated.
In short: R + W > N forces an overlapping node that always holds the freshest write; R + W ≤ N trades that guarantee for speed and availability.
quorum.py# MODEL: does this (N, R, W) configuration guarantee a read sees the latest write?
def quorum_config(N, R, W):
strong = (R + W > N) and (W > N // 2) # overlap + single-writer safety
return {
"N": N, "R": R, "W": W,
"read_write_overlap": R + W > N,
"strongly_consistent": strong,
"write_tolerates_failures": N - W, # nodes that can be down for a write
"read_tolerates_failures": N - R,
}
import json
print(json.dumps(quorum_config(3, 2, 2))) # classic strong: R+W=4>3
print(json.dumps(quorum_config(3, 1, 1))) # fast but eventual: R+W=2<=3
{"N": 3, "R": 2, "W": 2, "read_write_overlap": true, "strongly_consistent": true, "write_tolerates_failures": 1, "read_tolerates_failures": 1}
{"N": 3, "R": 1, "W": 1, "read_write_overlap": false, "strongly_consistent": false, "write_tolerates_failures": 2, "read_tolerates_failures": 2}
5 · Commit protocols — 2PC vs 3PC
Think about how a group of friends commit to a plan when everyone must be in or nobody goes. You don't just announce "we're going!" and hope — you first go around and ask each person "can you make it Saturday?" Only once everyone has said yes do you send the second message: "confirmed, it's on." That two-round dance — ask first, confirm second — is exactly two-phase commit (2PC), the classic way to make several database nodes either all apply a transaction or all abort it, with no node left in a half-done state.
Why can't you just tell everyone "commit" in one shot? Because a node might not be able to — its disk could be full, a constraint could be violated, it might be mid-crash. If you skipped the asking phase and half the nodes committed while half couldn't, your data is now inconsistent across machines, which is the one outcome atomicity exists to prevent. The prepare phase is the group asking "everyone able and willing?" and each participant promising it can commit if told to; the commit phase is the coordinator broadcasting the agreed outcome. It's genuinely correct — when it completes.
Here's the fatal flaw, and it's worth feeling viscerally. Suppose everyone votes yes, and then — right before sending the confirmation — the organiser's phone dies. Every friend is now stuck: they promised to go, they're holding the evening open, but they never heard the final word and they can't reach the organiser. They can't safely commit (maybe someone else backed out and the organiser was about to cancel) and can't safely abort (maybe it's on and they'd be no-shows). This is 2PC's blocking problem: a coordinator crash at the wrong instant leaves participants holding locks indefinitely. Three-phase commit (3PC) adds an extra "pre-commit" round so stuck participants can safely time out and decide for themselves — but it only works if you assume messages and clocks have bounded delays, an assumption the real internet happily violates. The common misconception is that 3PC "fixes" 2PC for production; in practice its assumptions rarely hold, which is why serious systems reach for consensus (§2) instead — a leader can die and a new majority simply elects a replacement and finishes the job. Read the two protocols below with that failure moment in mind: the whole story is what happens when the coordinator dies between the phases.
To make several nodes commit a transaction atomically, two-phase commit (2PC) uses a coordinator: phase 1 prepare (everyone votes yes/no and promises to be able to commit), phase 2 commit/abort (coordinator tells everyone the outcome). 2PC is correct but blocking: if the coordinator crashes after everyone voted yes, the participants are stuck holding locks, unsure whether to commit. Three-phase commit (3PC) inserts a pre-commit phase so participants can safely time out and proceed, making it non-blocking — at the cost of an extra round-trip and assumptions (bounded delays) that rarely hold, which is why real systems prefer consensus (Raft/Paxos) over 3PC.
Follow the blue arrows left-to-right — they are the two rounds the coordinator drives, one box per phase.
- The first box, "Prepare" is phase 1: the coordinator asks every participant whether it can commit and each promises to be ready.
- The middle box, "All vote yes?" (amber) is the gate: the outcome hinges entirely on whether every participant voted yes.
- The last box, "Commit / Abort" (green) is phase 2 — the coordinator broadcasts the single agreed outcome to everyone, so all nodes either apply the transaction or all abort it.
In short: Ask first (Prepare), then confirm once (Commit/Abort) — but if the coordinator dies between the two boxes, the participants are left blocked.
6 · Logical time — Lamport & vector clocks
You've almost certainly seen this bug in a chat app. Two people reply to a message at nearly the same moment, and the messages show up in a different order on your phone than on your friend's — or a reply appears above the thing it was replying to. The instinct is to fix it with timestamps: just sort by the clock, right? Except every phone's clock is slightly off, and "slightly" across machines is enough to sort events into an order that never actually happened. Wall-clock time is a lie you cannot base correctness on — and that uncomfortable fact is why distributed systems invented a different kind of clock entirely.
The insight is to stop asking "when did this happen?" (an unanswerable question with no shared clock) and instead ask "did this happen because of that?" — a question about causality, which you can answer without synchronised time. If event A sent a message that event B received, then A must have come before B, full stop, regardless of what any clock says. A Lamport clock is the simplest way to capture this: a plain counter that ticks on each event and jumps forward whenever you receive a message, guaranteeing that causes always get smaller numbers than their effects. It's enough to build a consistent total order — but it has a blind spot.
That blind spot is the crucial concept, so meet the analogy for it. Two friends in different cities each text a third friend at the same time, never having talked to each other. Neither text "caused" the other — they're concurrent. A Lamport clock will still hand them different numbers, falsely implying one came first. Vector clocks close the gap: instead of one counter, each node carries a whole vector — one counter per node — so comparing two stamps reveals not just an order but the truth: A happened-before B, B happened-before A, or the two are genuinely concurrent (neither vector dominates the other). The common mistake is assuming "concurrent" is a rare edge case you can ignore; in a replicated store it's the exact signal that two writes conflict and the system must consciously reconcile them rather than silently pick one. Read the mechanics below as a way of detecting that conflict honestly, instead of papering over it with untrustworthy timestamps.
There is no perfectly synchronised wall clock across machines, so you cannot order events by timestamp. Lamport clocks give a counter that respects causality (if A caused B, then A's counter < B's) but cannot tell whether two events are concurrent. Vector clocks fix that: each node keeps a vector of counters (one per node), so you can compare two events and decide happened-before, happened-after, or concurrent — which is exactly what a system needs to detect conflicting writes.
Read each panel as a comparison of two stamps: for each node's counter, ask which vector is bigger.
- On the left, A stamps its event
{A:1}and sends it; when B receives it, B takes the element-wise max and ticks itself, producing{A:1,B:2}. Because that green vector is ≥ A's in every slot,{A:1}happened-before it — a clean causal order. - On the right, A and B each act independently, giving
{A:1}and{B:1}. A's vector is ahead in the A-slot; B's is ahead in the B-slot — neither dominates the other. - That "neither dominates" is the payoff (in red): the events are concurrent. Wall clocks would falsely order them, but vector clocks reveal a genuine conflict the system must reconcile, not silently overwrite.
In short: One vector ≥ the other in every slot = a real order; neither ≥ the other = concurrent, which flags conflicting writes.
vector_clock.py# MODEL: each node holds a vector of counters. Compare two vectors to decide the
# causal relationship between the events they stamp.
def tick(clock, node): # local event on `node`
c = dict(clock); c[node] = c.get(node, 0) + 1; return c
def merge(local, incoming, node): # receive a message: merge then tick
c = {n: max(local.get(n, 0), incoming.get(n, 0)) for n in set(local) | set(incoming)}
c[node] = c.get(node, 0) + 1; return c
def relation(a, b):
nodes = set(a) | set(b)
le = all(a.get(n, 0) <= b.get(n, 0) for n in nodes)
ge = all(a.get(n, 0) >= b.get(n, 0) for n in nodes)
if le and ge: return "equal"
if le: return "a happened-before b"
if ge: return "b happened-before a"
return "concurrent (conflict!)"
# A does a local event, sends to B; B also did an independent local event
a1 = tick({}, "A") # {A:1}
b1 = tick({}, "B") # {B:1} -- independent of a1
b2 = merge(b1, a1, "B") # B receives A's message
print("a1 vs b2:", relation(a1, b2)) # a1 happened-before b2
print("a1 vs b1:", relation(a1, b1)) # concurrent -> a real conflict
a1 vs b2: a happened-before b
a1 vs b1: concurrent (conflict!)
7 · Consistency models & CAP → PACELC
Here's a moment you've lived as a user. You change your profile photo, the app cheerfully says "saved," you refresh — and the old photo is back. A minute later it's the new one again. Nothing is broken; you just read from a replica that hadn't caught up yet. That flicker is the whole subject of this section made visible: consistency is the set of promises a distributed store makes about which version of the truth a read is allowed to see. And the surprising, hard-won lesson of the field is that "always show the absolute latest" is not free — it's a premium feature you pay for in latency and availability.
The mental model that unlocks everything is a spectrum, not a switch. At the strong end, linearizable consistency makes the whole cluster behave as if there were a single copy: every read sees the most recent write, in one global order, no flicker ever. At the weak end, eventual consistency promises only that if writes stop, the replicas will eventually agree — in the meantime, reads can be stale (that returning-old-photo moment). Between them sit intermediate promises like causal consistency, which guarantees you'll at least see things in an order that respects cause and effect (your reply never appears before the message it answered), without paying for a full global order. Each step toward "strong" buys you a cleaner mental model and costs you speed and fault-tolerance.
Which brings us to the famous acronym everyone half-remembers. The common mistake is repeating "CAP means pick two of consistency, availability, partition-tolerance" — which is misleading, because partitions aren't optional (the network will split sometimes, per §1), so the real choice is only during a partition: stay consistent or stay available, you can't have both. But CAP quietly hides the everyday cost. PACELC tells the honest, fuller story: Partition → choose A or C; Else (the normal, no-partition case that's true 99.9% of the time) → choose Latency or Consistency. That "Else" clause is exactly the quorum trade-off from §4 generalised: even on a perfectly healthy network, a strongly-consistent read must wait for a quorum, which is slower than reading one nearby replica. Read the models and the CAP→PACELC extension below as the vocabulary for saying precisely what your system promises and what that promise costs on an ordinary Tuesday.
Consistency is a spectrum, not a switch. Linearizable (strongest): every operation appears to happen instantly at a single point — reads always see the latest write. Sequential/causal: weaker orderings that still respect some structure. Eventual (weakest): if writes stop, replicas eventually converge, but a read may be stale in the meantime. Stronger consistency costs latency and availability.
| Model | Guarantee | Cost | Example |
|---|---|---|---|
| Linearizable | reads see latest write, single global order | highest latency | etcd, Spanner |
| Causal | causally-related ops seen in order | moderate | some session stores |
| Eventual | replicas converge if writes stop | lowest latency, may be stale | DNS, Dynamo/Cassandra (tunable) |
CAP (from SD6): during a network Partition you must choose Consistency or Availability. PACELC extends it honestly: if Partition, choose A or C; Else (normal operation) choose Latency or Consistency. In other words, even with no partition, stronger consistency still costs latency — the quorum trade-off from §4, generalised.
Read it as a decision flow left-to-right, following the blue arrows through the two questions PACELC asks.
- The first box, "Partition?" (amber) is the branch point: is the network currently split? That is the only condition classic CAP ever considers.
- The middle box, "A or C" (red) is the partition case — while split you must give up either Availability or Consistency; you cannot keep both.
- The last box, "Else: L or C" (blue) is the payoff PACELC adds: even with no partition (the normal case), you still trade Latency against Consistency, because a strongly-consistent read must wait for a quorum.
In short: During a partition choose A or C; the rest of the time you are still choosing latency vs consistency — the everyday cost CAP hides.
✓ Checkpoint — you can move on when you can…
- Name three fallacies of distributed computing and the failure each causes.
- Explain how Raft elects a leader and commits a log entry via majority.
- Compute whether an (N, R, W) config is strongly consistent and why.
- Say why 2PC blocks and how consensus recovers where 2PC cannot.
- Use vector clocks to classify two events and place a system on CAP/PACELC.
Knowledge check
check yourselfA key-value store replicates each key to N = 5 nodes. The team wants a read to always see the latest committed write. Give one (R, W) pair that guarantees it, and explain the availability cost compared to R = W = 1.
Show answer
Two replicas each accept a write to the same key while partitioned; vector clocks later show the two writes are concurrent. Why can the database not simply keep the one with the later wall-clock time, and what does it do instead?
Show answer
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Majorities are the atom of consensus: a leader, a commit, and a strong read all hinge on 'more than half'.
Your task: Write a function that returns the majority size for an N-node cluster and use it to decide whether a candidate with V votes wins.
Requirements:
- Majority of N is N // 2 + 1
- A candidate wins iff its votes ≥ majority
- Show a winning and a losing vote count for an odd-sized cluster
- Show why 2 of 4 is not a majority
💡 Hint: Integer division makes this a one-liner; the subtlety is that even N wastes a node for the same fault tolerance.
Show solution
Majority is strictly more than half:
def majority(n):
return n // 2 + 1
def wins(n, votes):
return votes >= majority(n)
print("majority of 5:", majority(5)) # 3
print("3 of 5 wins:", wins(5, 3)) # True
print("2 of 5 wins:", wins(5, 2)) # False
print("2 of 4 wins:", wins(4, 2)) # False (need 3)Note 2 of 4 is not a majority — a 4-node cluster needs 3, the same as a 3-node cluster, yet tolerates only one failure. That is why consensus clusters are almost always odd-sized.
Context: R + W > N is the single most useful inequality in replicated storage: it tells you at a glance whether a config is strongly or eventually consistent.
Your task: Given N, R and W, report whether reads and writes overlap, whether the config is strongly consistent, and how many node failures each operation tolerates.
Requirements:
- read/write overlap holds iff R + W > N
- Report failures tolerated for writes (N − W) and reads (N − R)
- Classify strong vs eventual from the overlap
- Show one strong config and one eventual config for the same N
- Explain the availability cost of the strong config
💡 Hint: The overlap is purely R + W > N; the failure tolerance is what you give up to get it.
Show solution
The whole decision is the inequality R + W > N:
def classify(N, R, W):
overlap = R + W > N
return {
"overlap": overlap,
"consistency": "strong" if overlap else "eventual",
"write_failures_tolerated": N - W,
"read_failures_tolerated": N - R,
}
print("N=3 R=2 W=2:", classify(3, 2, 2)) # strong
print("N=3 R=1 W=1:", classify(3, 1, 1)) # eventualR=W=2 on N=3 overlaps (4 > 3) so it is strongly consistent, but each op tolerates only one node down. R=W=1 tolerates two failures and is fast, but a read can miss the latest write — eventual consistency. Same N, opposite ends of CAP.
Context: Vector clocks are how a distributed store decides whether two writes conflict — the prerequisite for any conflict-resolution policy.
Your task: Implement vector-clock tick and merge operations and a comparator that reports happened-before, happened-after, or concurrent for two event stamps.
Requirements:
- tick increments the acting node's own counter
- merge takes the element-wise max of two vectors, then ticks the receiver
- Comparator: a ≤ b element-wise → a before b; ≥ → after; neither → concurrent
- Show a causally-ordered pair and a concurrent pair
- Explain what 'concurrent' obliges the system to do
💡 Hint: Compare element-wise across the union of node keys; 'concurrent' is when neither ≤ nor ≥ holds.
Show solution
Tick on local events, merge on receive, compare element-wise:
def tick(clock, node):
c = dict(clock); c[node] = c.get(node, 0) + 1; return c
def merge(local, incoming, node):
keys = set(local) | set(incoming)
c = {n: max(local.get(n, 0), incoming.get(n, 0)) for n in keys}
c[node] = c.get(node, 0) + 1; return c
def relation(a, b):
keys = set(a) | set(b)
le = all(a.get(n, 0) <= b.get(n, 0) for n in keys)
ge = all(a.get(n, 0) >= b.get(n, 0) for n in keys)
if le and ge: return "equal"
if le: return "a before b"
if ge: return "b before a"
return "concurrent"
a1 = tick({}, "A")
b1 = tick({}, "B")
b2 = merge(b1, a1, "B") # B receives A's event
print(relation(a1, b2)) # a before b
print(relation(a1, b1)) # concurrenta1 is causally before b2 because B merged A's vector in. a1 and b1 are concurrent — neither vector dominates — which is precisely the signal that the two writes conflict and the system must reconcile them rather than pick by timestamp.
Context: Randomised election timeouts are Raft's trick for avoiding split votes; modelling a round shows why they converge.
Your task: Model one election round where each follower has a random timeout, the first to time out becomes a candidate and requests votes, and it wins iff it gathers a majority — repeating on a split vote.
Requirements:
- Give each node a random timeout; the minimum triggers candidacy
- The candidate collects votes; each node votes once per term
- A majority elects the leader; otherwise increment the term and retry
- Show a clean election and (by forcing a tie) a split vote resolving next term
- Explain why randomisation makes repeated splits unlikely
💡 Hint: Seed the RNG for reproducibility; a split is when no candidate reaches the majority in a term, so you bump the term and re-run.
Show solution
One reproducible election round with randomised timeouts:
import random
def election_round(nodes, term, rng):
timeouts = {n: rng.uniform(0.15, 0.30) for n in nodes} # randomised
candidate = min(timeouts, key=timeouts.get) # first to time out
need = len(nodes) // 2 + 1
# each other node votes for the candidate if it hasn't voted this term
votes = 1 + sum(1 for n in nodes if n != candidate) # all grant (no split here)
if votes >= need:
return {"term": term, "leader": candidate, "votes": votes}
return {"term": term, "leader": None, "votes": votes} # split -> retry
def elect(nodes, seed=1):
rng = random.Random(seed); term = 0
while True:
term += 1
r = election_round(nodes, term, rng)
if r["leader"] is not None:
return r
print(elect(["n1", "n2", "n3", "n4", "n5"]))The node with the smallest random timeout becomes candidate first and, gathering a majority, wins in term 1. Randomised timeouts are what make a split vote (two candidates dividing the votes so neither reaches a majority) rare: if it does happen, the term increments and a fresh round with new random timeouts almost always breaks the tie.
Context: The value of a quorum config only shows under failure: with the right R and W a read still finds the latest write even with a node down.
Your task: Simulate N replicas holding versioned values, perform a W-quorum write, take a node down, then perform an R-quorum read and show it still returns the latest version when R + W > N.
Requirements:
- Model each replica as a (version, value) cell
- A write updates any W reachable replicas to a new version
- Mark one replica as failed (unreachable)
- An R-quorum read returns the highest version among R reachable replicas
- Show the read sees the latest write iff R + W > N, and can miss it otherwise
💡 Hint: Because the write hit W replicas and the read consults R, R + W > N forces at least one replica in both sets — that overlap carries the latest version.
Show solution
Write to W replicas, fail one, read from R, take the newest version seen:
def simulate(N, R, W):
replicas = [{"version": 0, "value": None} for _ in range(N)]
# W-quorum write of version 1 to the first W replicas
for i in range(W):
replicas[i] = {"version": 1, "value": "v1"}
failed = N - 1 # take the LAST replica down
reachable = [i for i in range(N) if i != failed]
# R-quorum read: consult the LAST R reachable replicas, take highest version
read_set = reachable[-R:]
seen = max(replicas[i]["version"] for i in read_set)
return {
"N": N, "R": R, "W": W,
"R+W>N": R + W > N,
"read_set": read_set,
"read_saw_version": seen,
"saw_latest": seen == 1,
}
print(simulate(3, 2, 2)) # R+W=4>3 -> read sees version 1 even with a node down
print(simulate(3, 1, 1)) # R+W=2<=3 -> read reads replica 1, misses the writePrints saw_latest: True for (3,2,2) and saw_latest: False for (3,1,1). With N=3, R=2, W=2 the write covered two replicas and the read consults two of the two survivors — the sets must overlap, so the read returns version 1 despite the failure. With R=W=1 the read consults replica 1, which the single-replica write never touched, and sees the stale version 0. The overlap forced by R + W > N is the entire guarantee.
Context: Architecture reviews constantly ask 'what consistency does this give us and what does it cost' — PACELC is the vocabulary for answering precisely.
Your task: Build a small classifier that, given a system's partition behaviour and its no-partition behaviour, prints its PACELC class, and apply it to a strongly-consistent store, an eventually-consistent store, and a tunable one.
Requirements:
- Represent the two choices: on Partition (A or C), Else (L or C)
- Emit the PACELC label (e.g. PC/EC, PA/EL, PC/EL)
- Classify a linearizable store, an eventual store, and a tunable-quorum store
- Tie each classification back to its R/W or replication behaviour
- State that CAP alone would hide the Else (latency-vs-consistency) choice
💡 Hint: The Else branch is the everyday cost CAP omits: even with no partition, a strong read pays quorum latency.
Show solution
Encode both PACELC choices and classify three systems:
def pacelc(on_partition, on_else):
"""on_partition in {A, C}; on_else in {L, C}. Returns the PACELC label."""
assert on_partition in ("A", "C") and on_else in ("L", "C")
return f"P{on_partition}/E{on_else}"
systems = {
"etcd (Raft, linearizable)": pacelc("C", "C"), # PC/EC
"Cassandra (eventual, tunable)": pacelc("A", "L"), # PA/EL
"Spanner-like (consistent, fast)": pacelc("C", "L"), # PC/EL
}
for name, cls in systems.items():
print(f"{name:34s} -> {cls}")etcd chooses consistency on a partition and pays consistency latency otherwise (PC/EC). A tunable eventual store like Cassandra stays available and fast (PA/EL) by using low R/W quorums. A Spanner-like store keeps consistency on partition but is engineered for low latency otherwise (PC/EL). CAP alone would only tell you the first letter — PACELC surfaces the everyday latency-vs-consistency choice the quorum config in §4 actually makes.