AI EngineeringZero to ProductionHome·About·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.
▶ 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

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.

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

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 to majority Committed Apply
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

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.

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)

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.

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

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

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.

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

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