AI EngineeringZero to ProductionHome·About·What’s new·Contact
System Design · Chapter SD12

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.

⏱️ ~4 hours🧪 5 labs🎯 Beginner→Expert
🌱 Start here — from zeroDistributed systems theory, from scratch. One machine is easy; the trouble starts when state lives on many machines connected by a network that drops and delays messages and where nodes fail independently. This chapter is the theory that makes multi-node systems correct: how nodes agree (consensus), how many replicas must acknowledge a read/write (quorums), how to order events without a shared clock (logical time), and what consistency you can actually promise. The quorum and vector-clock labs are runnable models, not a real cluster.

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.
📋 Before you startThis page builds on: Scalability & CAP. Jumping in? Skim those first — or open the recap.
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.
▶ Runnable companionThe quorum calculator and vector-clock tracker are plain-Python models of the logic — they compute the same overlap guarantees and causal orderings a real cluster relies on, without any network.
Cross-link — CAP from SD6This chapter completes the CAP discussion from SD6 · Scalability & CAP. SD6 introduced the C/A/P trade-off; here we make it precise with quorums and extend it to PACELC, which also covers the latency cost of consistency when there is no 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.

same machine — like asking your roommate caller callee instant · reliable · one answer across a network — like mailing a letter abroad service A service B may be lost · late · duplicated …and you can't tell which happened no timeout ⇒ caller thread hangs forever every fallacy = one of these failures denied The network is not a function call. On one machine a call returns instantly with a single answer; across a network the request can vanish, arrive twice, or hang — and if you assumed otherwise (no timeout), your own service falls over.
🗺️ How to read this diagram

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

FallacyRealityWhat it causes if believed
The network is reliablepackets drop; nodes partitionno retries/timeouts → hangs
Latency is zerocross-region RTT is tens of mschatty calls → slow pages
Bandwidth is infinitelinks saturatehuge payloads → congestion
Topology doesn't changenodes come and gohard-coded hosts → outages
Transport cost is zeroserialization + I/O cost real timeignored overhead → surprise bills
Every fallacy is a design requirement in disguiseBecause the network is unreliable, you need timeouts, retries and idempotency. Because latency is non-zero, you batch and cache. The rest of this chapter is what you build once you stop believing the fallacies.

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.

Leader Append tomajority Committed Apply
🗺️ How to read this diagram

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.

RaftPaxos
Goalagree on a replicated logagree on a value (multi-Paxos: a log)
Leaderexplicit, electedimplicit (proposer)
Reputationdesigned for understandabilitycorrect but hard to reason about
Used byetcd, Consul, CockroachDBChubby, 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.

Follower waits to hear a leader timeout (randomised) Candidate term++ · requests votes majority Leader accepts all writes split vote → new term, retry Three states, one safe leader. A follower whose randomised timer fires first becomes a candidate; a majority makes it leader. A rare split vote just bumps the term and re-runs — the one-vote-per-term rule means two leaders can never coexist.
🗺️ How to read this diagram

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.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Step 1 · Majority vote for a leader (model)
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}
Why clusters are usually odd-sizedA 4-node cluster needs 3 for a majority and so tolerates only 1 failure — exactly the same as a 3-node cluster, but at the cost of an extra node. A 5-node cluster needs 3 and tolerates 2. Odd sizes give the best failure tolerance per node, which is why you almost always see 3- or 5-node consensus clusters, rarely 4.

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.

N=5, W=3, R=3 → R+W=6 > 5 n1 n2 n3 n4 n5 W = {n1,n2,n3} R = {n3,n4,n5} n3 in both → read sees the write ✓ overlap forced → strong consistency N=5, W=1, R=1 → R+W=2 ≤ 5 n1 n2 n3 n4 n5 W = {n1} R = {n5} no shared node → read misses it ✗ may be stale → eventual, but faster Overlap is the whole guarantee. When W + R exceeds N the write set and read set cannot avoid sharing a node (n3), so the read always catches the newest write; drop below N and they can miss entirely — faster and more available, but possibly stale.
🗺️ How to read this diagram

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.

Step 2 · A quorum calculator (model)
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}
The quorum knob IS the CAP knobR=W=2, N=3 gives strong consistency but a write needs 2 acks (less available under failure). R=W=1 answers from any single node (fast, highly available) but a read can return stale data. There is no free lunch — you are choosing a point on the CAP curve per operation.

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.

Prepare All vote yes? Commit / Abort
🗺️ How to read this diagram

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.

2PC's blocking window is the whole point of consensusThe reason etcd/Spanner use Raft/Paxos rather than plain 2PC is that consensus keeps making progress with a majority even when the coordinator (leader) dies — a new leader is elected. 2PC alone has no such recovery, so a coordinator crash can freeze the transaction.

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.

B receives A's message A: {A:1} B: {B:1} {A:1,B:2} {A:1} ≤ {A:1,B:2} A happened-before B ✓ two independent events a: {A:1} b: {B:1} A ahead here B ahead here neither dominates concurrent → conflict! Domination decides order. If one vector is ≥ the other in every slot, that event happened-after (left: B merged A, so A→B). If each is ahead in its own slot and neither dominates, the events are concurrent — the exact signal of a write conflict.
🗺️ How to read this diagram

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.

Step 3 · Vector clocks: ordering events (model)
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!)
Concurrent = the system must decideWhen vector clocks say two writes are concurrent, neither caused the other, so the database cannot pick a winner by causality — it must apply a conflict-resolution policy (last-writer-wins, merge, or hand both versions to the app, as Dynamo does with siblings).

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.

ModelGuaranteeCostExample
Linearizablereads see latest write, single global orderhighest latencyetcd, Spanner
Causalcausally-related ops seen in ordermoderatesome session stores
Eventualreplicas converge if writes stoplowest latency, may be staleDNS, 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.

Partition? A or C Else: L or C
🗺️ How to read this diagram

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.

PACELC names the everyday cost CAP hidesCAP only talks about the rare partition. PACELC adds the honest everyday truth: a strongly-consistent read needs a quorum (§4), which is slower than reading one nearby replica. Most systems are "PC/EL" (consistent on partition, low-latency otherwise) or "PA/EL" (available and fast, eventually consistent).

✓ 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 yourself
✓ Knowledge check

A 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
Any pair with R + W > 5 works — e.g. R = 3, W = 3 (or R = 2, W = 4). The overlap guarantees the read set intersects the last write set, so the read sees the latest value. The cost: a write now needs 3 acks (it fails if 3+ nodes are down) and a read needs 3 responses, whereas R = W = 1 answers from any single node — far more available and lower latency, but a read can return stale data.
✓ Knowledge check

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
Wall clocks across machines are not reliably synchronised, so "later timestamp" is not a sound ordering — and vector clocks have already established the writes are causally concurrent (neither happened-before the other), so there is no correct winner by causality. The database must apply an explicit conflict-resolution policy: last-writer-wins (lossy), a merge function (e.g. CRDT semantics), or surface both versions as siblings for the application to reconcile.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Compute a majority quorumBeginner

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.

Exercise 2 · Classify an (N, R, W) configurationIntermediate

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))   # eventual

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

Exercise 3 · Order events with vector clocksAdvanced

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))       # concurrent

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

Exercise 4 · A leader-election round with randomised timeoutsExpert

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.

Exercise 5 · Simulate a quorum read/write under node failureProfessional

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 write

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

Exercise 6 · Place three real systems on CAP/PACELCIndustry scenario

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.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in