Chapter 6: Distributed Systems Theory

Consensus, Paxos, Raft, Vector Clocks, Lamport Timestamps

Introduction

Distributed systems are fundamentally about coordination across independent machines that communicate via an unreliable network. This chapter explores the theoretical foundations that make distributed consensus possible—and the impossibility results that constrain what we can achieve.

Interview Signal: Senior/Staff engineers are expected to understand WHY consensus is hard, not just "use etcd." Expect deep-dives on Raft mechanics, FLP impossibility, and tradeoffs between Paxos/Raft/ZAB.

Key questions this chapter answers:

  • Why can't we rely on physical clocks?
  • How do Lamport timestamps and vector clocks order events without synchronized time?
  • What makes consensus theoretically hard (FLP, Two Generals)?
  • How do Paxos, Raft, and ZAB achieve consensus in practice?
  • What ordering guarantees can distributed systems provide?

Why Distributed Systems Are Hard

The Two Generals Problem

Two armies need to coordinate an attack. They can only communicate via messengers who may be captured. The problem: there is no protocol that guarantees both generals will attack simultaneously in the presence of message loss.

graph LR A[General A] -->|Attack at dawn?| B[General B] B -->|Acknowledged| A A -->|Ack received?| B B -->|Ack-ack received?| A style A fill:#e74c3c style B fill:#e74c3c subgraph "Infinite Regression" A -.->|Need ack of ack of ack...| B end
Misconception: "We can solve this with retries."
Reality: No matter how many acknowledgment rounds, the last sender can never know if their final message was received. This is an impossibility result.

The Byzantine Generals Problem

Extension where some generals may be traitors sending conflicting information. Requires 3f+1 nodes to tolerate f Byzantine faults. Much harder than crash faults.

Failure Model Description Nodes Required Examples
Crash Faults Node stops responding 2f+1 for f faults Raft, Paxos, ZAB
Byzantine Faults Node behaves maliciously 3f+1 for f faults PBFT, Blockchain consensus
Omission Faults Node drops messages Between crash & Byzantine Network partitions

Asynchronous vs Synchronous Models

graph TD A[System Models] --> B[Synchronous] A --> C[Partially Synchronous] A --> D[Asynchronous] B --> B1["Known bounds on:
- Message delay
- Processing time
- Clock drift"] C --> C1["Eventually synchronous
(Real systems)"] D --> D1["No timing assumptions
(FLP applies)"] style B fill:#2ecc71 style C fill:#f39c12 style D fill:#e74c3c
  • Synchronous: Known upper bounds on message delay and processing time. Consensus is solvable.
  • Asynchronous: No timing assumptions. FLP impossibility applies—deterministic consensus is impossible with even one crash fault.
  • Partially Synchronous: Real-world model. System is synchronous most of the time but may violate bounds temporarily. Practical algorithms (Raft, Paxos) work here.
Interview Signal: "Why can't we use timeouts for failure detection in asynchronous systems?" → Unbounded delays mean you can't distinguish slow from crashed. Practical systems use partial synchrony and accept temporary unavailability.

Partial Failures

The defining challenge: some components fail while others continue. Unlike a single machine where everything crashes together, distributed systems have nondeterministic partial failures.

graph LR C[Client] -->|Request| L[Load Balancer] L -->|OK| S1[Server 1] L -->|Timeout| S2[Server 2 - Crashed] L -->|OK but slow| S3[Server 3] L -->|Network partition| S4[Server 4 - Alive but unreachable] style S1 fill:#2ecc71 style S2 fill:#e74c3c style S3 fill:#f39c12 style S4 fill:#9b59b6

Key insight: You cannot distinguish between:

  • A crashed node
  • A slow node
  • A partitioned node
  • A node whose response was lost

Time in Distributed Systems

Why Physical Clocks Fail

Physical clocks drift due to temperature, manufacturing variance, and relativistic effects. Even with NTP, you get:

  • Clock skew: Clocks disagree on current time
  • Clock drift: Clocks progress at different rates
  • NTP accuracy: ~1-50ms on LAN, worse on WAN
sequenceDiagram participant A as Server A 12:00:00.100 participant B as Server B 12:00:00.050 participant C as Server C 12:00:00.200 Note over A,C: Clock skew = 150ms between B and C A->>B: Event X at 12:00:00.100 B->>C: Event Y at 12:00:00.050 Note over C: C sees Y "before" X
even though X happened first!
Misconception: "NTP gives us synchronized clocks."
Reality: NTP provides best-effort synchronization with millisecond-level error. Not sufficient for causality reasoning or distributed transactions.

GPS and Atomic Clocks

High-precision systems (Google Spanner, AWS Time Sync Service) use:

  • GPS receivers: ~100ns accuracy
  • Atomic clocks: Nanosecond precision
  • TrueTime API (Spanner): Returns time interval [earliest, latest] with confidence bounds
# Google Spanner TrueTime API tt = TrueTime.now() print(tt.earliest) # 12:00:00.123456789 print(tt.latest) # 12:00:00.123456999 # Guaranteed: actual time is in [earliest, latest]

Cost tradeoff: GPS/atomic clocks are expensive. Most systems use logical clocks instead.

Logical Clocks

If we can't trust physical time, how do we order events? Logical clocks capture causality without synchronized clocks.

Lamport Timestamps

Invented by Leslie Lamport (1978). Each process maintains a counter that captures the happens-before relation.

Happens-Before Relation (→)

  • Local ordering: If a and b are events in the same process, and a occurs before b, then a → b
  • Message ordering: If a is the send event and b is the receive event, then a → b
  • Transitivity: If a → b and b → c, then a → c

Lamport Clock Rules

  1. Initial: Each process starts with clock = 0
  2. Local event: Increment clock before event
  3. Send message: Include clock value in message
  4. Receive message: clock = max(local_clock, message_clock) + 1
sequenceDiagram participant P1 as Process P1 (Clock) participant P2 as Process P2 (Clock) participant P3 as Process P3 (Clock) Note over P1: 1 P1->>P1: Event A (1) Note over P1: 2 P1->>P2: Message (2) Note over P2: 1 Note over P2: max(1,2)+1=3 P2->>P2: Receive (3) Note over P2: 4 P2->>P3: Message (4) Note over P3: 1 Note over P3: 2 Note over P3: max(2,4)+1=5 P3->>P3: Receive (5)

Limitations of Lamport Timestamps

If a → b, then L(a) < L(b). BUT the converse is NOT true: L(a) < L(b) does NOT imply a → b.

graph TD A["Event A
P1: L=5"] B["Event B
P2: L=7"] A -.->|"L(A) < L(B)
but A ∦ B"| B Note[Concurrent events!
Cannot determine order] style A fill:#3498db style B fill:#3498db style Note fill:#f39c12
Misconception: "Lamport timestamps tell us which event happened first."
Reality: They only capture potential causality. Cannot detect concurrent events.

Real-World Usage

  • Distributed debugging: Reconstruct event ordering from logs
  • Conflict-free ordering: Break ties in distributed databases (e.g., last-write-wins with Lamport timestamp)
  • Snapshot isolation: Assign monotonic timestamps to transactions

Vector Clocks

Vector clocks extend Lamport timestamps to detect concurrency. Each process maintains a vector of clocks for all processes.

Vector Clock Rules

  1. Initial: V[i] = [0, 0, ..., 0] for all processes
  2. Local event at Pi: V[i][i]++
  3. Send message from Pi: Include V[i] in message
  4. Receive message at Pj: V[j][k] = max(V[j][k], V_msg[k]) for all k, then V[j][j]++
sequenceDiagram participant P1 as P1: [0,0,0] participant P2 as P2: [0,0,0] participant P3 as P3: [0,0,0] Note over P1: [1,0,0] P1->>P1: Event A Note over P1: [2,0,0] P1->>P2: Msg (2,0,0) Note over P2: [2,1,0] P2->>P2: Receive Note over P3: [0,0,1] P3->>P3: Event B Note over P2: [2,2,0] P2->>P3: Msg (2,2,0) Note over P3: [2,2,2] P3->>P3: Receive

Comparing Vector Clocks

For vectors V1 and V2:

  • V1 < V2 (happens-before) iff V1[i] ≤ V2[i] for all i, and V1 ≠ V2
  • V1 = V2 (same event) iff V1[i] = V2[i] for all i
  • V1 || V2 (concurrent) iff neither V1 < V2 nor V2 < V1
# Example comparisons V1 = [2, 1, 0] V2 = [3, 2, 1] V3 = [1, 3, 0] V1 < V2 # True: V1 happened before V2 V1 || V3 # True: Concurrent (V1[1]=1 < V3[1]=3, but V1[0]=2 > V3[0]=1)

Space Overhead

Vector clocks require O(N) space per event, where N = number of processes. Problematic for large-scale systems (millions of clients).

Clock Type Space Detects Causality Detects Concurrency
Lamport O(1) Yes (one direction) No
Vector O(N) Yes (both directions) Yes
Dotted Version Vector O(K), K=replicas Yes Yes

Real-World Usage

  • Dynamo/Riak: Vector clocks for detecting conflicting writes (siblings)
  • Distributed debugging: ShiViz, Dinv visualize causality with vector clocks
  • Version control: Git's commit graph is essentially a vector clock (SHA hashes)

Hybrid Logical Clocks (HLC)

Combines physical time and logical counters to get the best of both worlds. Used in CockroachDB and Google Spanner's external consistency.

HLC Structure

HLC = (pt, lc) where:

  • pt: Physical time (wall clock)
  • lc: Logical counter (disambiguates events at same pt)

HLC Rules

  1. Send/Local event:
    • pt = max(pt, physical_time)
    • If pt increased, lc = 0, else lc++
  2. Receive message:
    • pt = max(pt, msg.pt, physical_time)
    • If pt = msg.pt, lc = max(lc, msg.lc) + 1
    • Else if pt = physical_time, lc++
    • Else lc = 0
sequenceDiagram participant P1 as P1: (pt, lc) participant P2 as P2: (pt, lc) Note over P1: (100, 0)
Physical time=100 P1->>P1: Event A Note over P1: (100, 1)
Same physical time, lc++ P1->>P2: Msg (100, 1) Note over P2: Physical time=99 Note over P2: (100, 2)
pt=max(99,100)=100
lc=max(0,1)+1=2 P2->>P2: Receive

Advantages

  • Bounded drift from physical time: HLC closely tracks wall clock (unlike pure logical clocks)
  • O(1) space: No vector overhead
  • Efficient comparisons: Lexicographic order (pt, lc)
  • Human-readable: Timestamps are approximate wall-clock times
Interview Signal: "Why does CockroachDB use HLC instead of Lamport or Vector clocks?" → HLC gives causality guarantees with O(1) space and timestamps that are close to physical time, enabling time-travel queries and MVCC garbage collection.

Real-World Usage

  • CockroachDB: HLC for MVCC timestamps and external consistency
  • MongoDB (in research): Causal consistency with HLC
  • Academic systems: OrbitDB, ScyllaDB experiments

The Consensus Problem

Consensus is the foundation of distributed coordination. Formal problem: N processes propose values; all must agree on one value.

Safety Properties

  1. Agreement: No two correct processes decide differently
  2. Validity: If all processes propose v, then v is the only possible decision (no trivial solutions)
  3. Termination: Every correct process eventually decides (liveness)
graph TD P1[Process 1
proposes A] --> C{Consensus
Protocol} P2[Process 2
proposes B] --> C P3[Process 3
proposes A] --> C P4[Process 4
proposes A] --> C C --> D1[Process 1
decides A] C --> D2[Process 2
decides A] C --> D3[Process 3
decides A] C --> D4[Process 4
decides A] style C fill:#e74c3c style D1 fill:#2ecc71 style D2 fill:#2ecc71 style D3 fill:#2ecc71 style D4 fill:#2ecc71

Why Consensus is Fundamental

Many distributed problems reduce to consensus:

  • Leader election: Agree on which node is the leader
  • State machine replication: Agree on order of operations
  • Atomic commit: Agree on whether to commit or abort a transaction
  • Group membership: Agree on which nodes are in the cluster
Interview Signal: "How does Raft achieve linearizability?" → Consensus on log entry order ensures all replicas apply operations in the same order, giving the appearance of a single machine (linearizability).

Consensus vs Atomic Broadcast

These are equivalent problems:

  • Consensus: Agree on a single value
  • Total Order Broadcast: Agree on a sequence of values (order of messages)

Atomic broadcast = repeated consensus (one consensus per message).

Paxos

Invented by Leslie Lamport (1989, published 1998). The first practical consensus algorithm. Notoriously difficult to understand and implement correctly.

Roles

  • Proposers: Propose values
  • Acceptors: Vote on proposals (quorum = majority)
  • Learners: Learn the chosen value

In practice, nodes often play multiple roles.

Basic Paxos: Two Phases

Phase 1: Prepare/Promise

  1. Proposer selects proposal number n (must be unique and increasing)
  2. Sends PREPARE(n) to majority of acceptors
  3. Acceptors respond:
    • If n > highest seen, promise not to accept proposals < n
    • Return any previously accepted value (if any)

Phase 2: Accept/Accepted

  1. Proposer receives promises from majority
  2. Chooses value:
    • If any acceptor returned a value, use the value with highest proposal number
    • Otherwise, use proposer's own value
  3. Sends ACCEPT(n, value) to acceptors
  4. Acceptors accept if they haven't promised a higher number
  5. Learners learn the value once majority has accepted
sequenceDiagram participant P as Proposer participant A1 as Acceptor 1 participant A2 as Acceptor 2 participant A3 as Acceptor 3 participant L as Learner Note over P: Prepare Phase P->>A1: PREPARE(n=1) P->>A2: PREPARE(n=1) P->>A3: PREPARE(n=1) A1->>P: PROMISE(n=1, no prior value) A2->>P: PROMISE(n=1, no prior value) Note over P: Majority achieved (2/3) Note over P: Accept Phase P->>A1: ACCEPT(n=1, value=X) P->>A2: ACCEPT(n=1, value=X) P->>A3: ACCEPT(n=1, value=X) A1->>L: ACCEPTED(n=1, X) A2->>L: ACCEPTED(n=1, X) Note over L: Value X chosen!

Failure Scenarios

Dueling Proposers

Two proposers compete with increasing proposal numbers, preventing progress (livelock).

sequenceDiagram participant P1 as Proposer 1 participant P2 as Proposer 2 participant A as Acceptors P1->>A: PREPARE(n=1) A->>P1: PROMISE(n=1) P2->>A: PREPARE(n=2) A->>P2: PROMISE(n=2) P1->>A: ACCEPT(n=1, X) A->>P1: REJECT (promised n=2) Note over P1: Retry with n=3 P1->>A: PREPARE(n=3) A->>P1: PROMISE(n=3) P2->>A: ACCEPT(n=2, Y) A->>P2: REJECT (promised n=3) Note over A: No progress!

Solution: Randomized backoff or leader election (Multi-Paxos).

Multi-Paxos: Optimization for Log Replication

Running Basic Paxos for every log entry is expensive. Multi-Paxos optimizations:

  • Stable leader: One proposer is elected leader; skips Prepare phase for subsequent proposals
  • Batching: Leader proposes multiple entries in one round
  • Pipelining: Don't wait for Accept responses before sending next proposal
graph TD A[Multi-Paxos] --> B[Phase 1: Leader Election] A --> C[Phase 2: Normal Operation] B --> B1[Run Basic Paxos once
to elect leader] C --> C1[Leader proposes entries
without Prepare phase] C --> C2[Accept phase only] style B fill:#e74c3c style C fill:#2ecc71

Why Paxos is Hard

  • Opaque reasoning: Why does accepting the highest-numbered prior value work? Requires proof.
  • Many variants: Basic Paxos, Multi-Paxos, Fast Paxos, Cheap Paxos, Byzantine Paxos...
  • Implementation details: Lamport's paper left many practical issues unspecified (log compaction, leadership, membership changes)
  • Terminology confusion: Proposals, ballots, rounds, instances...
Misconception: "Paxos is used in production."
Reality: Most systems use Raft or ZAB because they're easier to understand and implement. Google's Chubby used Paxos but the implementation was notoriously complex.

Real-World Usage

  • Google Chubby: Early Paxos implementation (later evolved)
  • Apache Cassandra: Uses Paxos for lightweight transactions (LWT)
  • Azure Cosmos DB: Internally uses Paxos variants
Interview Signal: "Explain why Paxos requires a majority quorum." → Any two majorities overlap in at least one node. This ensures a new proposer learns about any previously accepted value, preventing conflicting decisions.

Raft

Designed by Diego Ongaro and John Ousterhout (2014) to be more understandable than Paxos. "In search of an understandable consensus algorithm."

Design Philosophy

  • Understandability: Decompose problem into leader election, log replication, safety
  • Strong leader: Log entries only flow from leader to followers (simplifies reasoning)
  • Term-based epochs: Time divided into terms with at most one leader per term
graph LR T1[Term 1
Leader A] --> T2[Term 2
No Leader
Election fails] T2 --> T3[Term 3
Leader B] T3 --> T4[Term 4
Leader B] style T1 fill:#2ecc71 style T2 fill:#f39c12 style T3 fill:#2ecc71 style T4 fill:#2ecc71

Raft Roles

  • Leader: Handles all client requests, replicates log to followers
  • Follower: Passive; responds to RPCs from leader and candidates
  • Candidate: Follower that times out becomes candidate to request votes
stateDiagram-v2 [*] --> Follower Follower --> Candidate: Timeout, no heartbeat Candidate --> Leader: Receives majority votes Candidate --> Follower: Discovers current leader or higher term Leader --> Follower: Discovers higher term Candidate --> Candidate: Split vote, retry Follower --> Follower: Receives AppendEntries

Leader Election

Election Process

  1. Follower increments current term
  2. Transitions to Candidate
  3. Votes for itself
  4. Sends RequestVote RPCs to all peers
  5. Waits for:
    • Win: Receives votes from majority → becomes Leader
    • Lose: Receives AppendEntries from valid leader → becomes Follower
    • Timeout: No winner (split vote) → new election
sequenceDiagram participant F as Follower participant C as Candidate participant P1 as Peer 1 participant P2 as Peer 2 participant P3 as Peer 3 Note over F: Election timeout F->>C: Become Candidate
term=2, vote for self C->>P1: RequestVote(term=2) C->>P2: RequestVote(term=2) C->>P3: RequestVote(term=2) P1->>C: VoteGranted P2->>C: VoteGranted Note over C: Majority achieved (3/5)
Become Leader

Split Vote Prevention

Randomized election timeouts (e.g., 150-300ms) reduce likelihood of split votes.

graph TD A[Multiple followers timeout simultaneously] --> B[Split vote] B --> C[Randomized timeout] C --> D[One candidate usually wins before others timeout] style B fill:#e74c3c style D fill:#2ecc71

Log Replication

AppendEntries RPC

Leader sends AppendEntries RPCs to followers:

  • Heartbeat: Empty AppendEntries (no log entries) to maintain authority
  • Log replication: Include new entries to replicate

Log Structure

Each log entry contains:

  • Command: State machine command
  • Term: Term when entry was created
  • Index: Position in log
graph LR subgraph "Leader Log" L1["1: set x=1
(term 1)"] L2["2: set y=2
(term 1)"] L3["3: set z=3
(term 2)"] L4["4: set x=4
(term 3)"] end subgraph "Follower 1 Log" F1["1: set x=1
(term 1)"] F2["2: set y=2
(term 1)"] F3["3: set z=3
(term 2)"] F4["4: set x=4
(term 3)"] end subgraph "Follower 2 Log (lagging)" F21["1: set x=1
(term 1)"] F22["2: set y=2
(term 1)"] end style L4 fill:#2ecc71 style F4 fill:#2ecc71

Commitment Rule

Leader commits entry when it's replicated on a majority of servers. Once committed, entry is durable and will be applied to all state machines.

sequenceDiagram participant C as Client participant L as Leader participant F1 as Follower 1 participant F2 as Follower 2 participant F3 as Follower 3 C->>L: set x=5 Note over L: Append to local log
index=10, term=3 L->>F1: AppendEntries(10, x=5) L->>F2: AppendEntries(10, x=5) L->>F3: AppendEntries(10, x=5) F1->>L: Success F2->>L: Success Note over L: Majority replicated (3/5)
Commit index=10 L->>C: OK Note over L: Next heartbeat informs followers L->>F1: AppendEntries(commitIndex=10) L->>F2: AppendEntries(commitIndex=10)

Safety Properties

Election Restriction

A candidate can only become leader if its log contains all committed entries. Voters reject candidates with less up-to-date logs.

Log comparison:

  • Log A is more up-to-date than Log B if:
    • Last entry has higher term, OR
    • Same term but longer log
# Voter grants vote only if: if (candidate_last_term > my_last_term) or (candidate_last_term == my_last_term and candidate_log_length >= my_log_length): grant_vote() else: reject()

Leader Completeness

If a log entry is committed in term T, it will be present in the logs of all leaders for terms > T.

State Machine Safety

If a server has applied a log entry at index i, no other server will apply a different command at index i.

Interview Signal: "Why can't a Raft leader commit entries from previous terms by counting replicas?" → A new leader might overwrite uncommitted entries from old terms. Leaders can only commit entries from their current term by counting replicas.

Log Inconsistencies and Repair

Followers' logs may diverge due to crashes. Leader forces followers' logs to match its own.

Inconsistency Example

graph TD subgraph "Leader Log (term 3)" L1[1: x=1, t=1] L2[2: y=2, t=1] L3[3: z=3, t=2] L4[4: a=4, t=3] end subgraph "Follower Log (crashed in term 2)" F1[1: x=1, t=1] F2[2: y=2, t=1] F3[3: WRONG, t=2] end style L4 fill:#2ecc71 style F3 fill:#e74c3c

Repair Process

  1. Leader sends AppendEntries with prevLogIndex and prevLogTerm
  2. Follower checks if entry at prevLogIndex matches prevLogTerm
  3. If mismatch, follower rejects
  4. Leader decrements nextIndex and retries
  5. When match found, follower deletes conflicting entries and appends new ones
sequenceDiagram participant L as Leader participant F as Follower L->>F: AppendEntries(prevIndex=3, prevTerm=2, entry 4) F->>L: Reject (term mismatch at index 3) L->>F: AppendEntries(prevIndex=2, prevTerm=1, entries 3-4) F->>L: Success Note over F: Delete entry 3
Append entries 3-4

Membership Changes

Adding/removing servers safely is tricky. Raft uses joint consensus (C_old,new configuration).

graph LR A[C_old] --> B[C_old,new
Joint consensus] B --> C[C_new] style B fill:#f39c12

During C_old,new:

  • Log entries replicated to all servers in both configurations
  • Any server from either configuration can be leader
  • Agreement requires majorities from BOTH C_old and C_new

Raft vs Paxos

Aspect Raft Paxos (Multi-Paxos)
Understandability Designed for clarity Notoriously complex
Leadership Strong leader (only leader proposes) Weak leader (others can propose)
Terms/Epochs Explicit term numbers Implicit in proposal numbers
Log gaps No gaps allowed Gaps possible (out-of-order commits)
Membership changes Joint consensus (well-specified) Underspecified in original paper
Performance Similar (slight overhead from strong leader) Similar (flexibility may help in some scenarios)

Real-World Usage

  • etcd: Kubernetes' coordination service
  • Consul: Service discovery and configuration
  • TiKV: Distributed key-value store (TiDB's storage layer)
  • CockroachDB: Range replication uses Raft
  • Dgraph: Graph database uses Raft for replication
Interview Signal: "How does Raft handle network partitions?" → Minority partition cannot elect a leader (can't achieve majority). Majority partition continues to make progress. When partition heals, minority followers catch up by appending leader's log.

Raft Log Compaction & Snapshotting

Problem: In production, the log grows unbounded. After 1 million entries, a new follower joining the cluster would take forever to replay the entire log from index 1. Memory usage grows indefinitely.

Solution: Periodic Snapshots

Each server periodically takes a snapshot of its state machine at log index N, then discards all log entries ≤ N. This prevents unbounded log growth.

graph LR subgraph "Before Snapshot" L1[1: x=1] --> L2[2: y=2] L2 --> L3[3: z=3] L3 --> L4[4: x=4] L4 --> L5[...1M entries...] end subgraph "After Snapshot at index 1M" S[Snapshot:
x=100, y=200, z=300
lastIndex=1M, lastTerm=5] S --> L6[1M+1: new entry] L6 --> L7[1M+2: new entry] end style S fill:#2ecc71

InstallSnapshot RPC

When a follower is too far behind (the log entries it needs have been discarded), the leader sends the entire snapshot using the InstallSnapshot RPC.

sequenceDiagram participant L as Leader participant F as Slow Follower Note over F: Last log index: 500K Note over L: Snapshot at 1M
Log starts at 1M+1 L->>F: AppendEntries(prevIndex=1M, ...) F->>L: ❌ Reject (I only have up to 500K) Note over L: Follower too far behind
Send snapshot L->>F: InstallSnapshot(lastIndex=1M, lastTerm=5, data) Note over F: Discard log 1-500K
Apply snapshot state F->>L: ✓ OK L->>F: AppendEntries(prevIndex=1M, entry 1M+1) F->>L: ✓ Success

When to Snapshot

Snapshotting policy is configurable per deployment:

  • Entry-based: Every 10,000 entries (typical for small state machines)
  • Size-based: When log exceeds 100MB (common in production)
  • Time-based: Every hour (for predictable background load)
# etcd snapshotting config --snapshot-count=10000 # Snapshot every 10K entries # Raft library example if log.size() > 100_000_000: # 100MB snapshot = state_machine.create_snapshot() log.compact(snapshot.last_included_index)

Safety Guarantees

Each snapshot includes:

  • Last included index: Highest log entry included in snapshot
  • Last included term: Term of that entry
  • State machine data: Complete state (e.g., key-value pairs)

When a follower receives InstallSnapshot:

  1. If snapshot is older than its current state, ignore
  2. Discard its log up to last_included_index
  3. Replace state machine with snapshot data
  4. Update metadata (currentTerm, lastApplied)

Performance Impact

// Without snapshotting (1 year uptime)
Log entries:           100M entries (~10GB memory)
New follower join:     45 minutes (replay 100M entries)
Memory per server:     10GB (log) + state machine

// With snapshotting (every 10K entries)
Log entries:           ~10K entries (~1MB memory)
New follower join:     30 seconds (InstallSnapshot + replay 10K)
Memory per server:     1MB (log) + state machine
Interview Callout: "Always mention log compaction when discussing Raft in production. Without it, memory grows unbounded and new nodes joining the cluster take hours to catch up. etcd snapshots every 10K entries by default. The InstallSnapshot RPC is critical for bringing slow followers up to date when their needed log entries have been discarded."

Real-World Example: etcd

etcd uses Raft and automatically snapshots the key-value store:

  • Default: Snapshot every 10,000 applied entries
  • Snapshot stored in db file (BoltDB backend)
  • Old log entries compacted, reducing memory usage
  • New members join by receiving snapshot + recent log

Viewstamped Replication (VR)

Invented by Barbara Liskov and Brian Oki (1988), predating Paxos publication. Similar to Raft but less well-known.

Key Concepts

  • View: Analogous to Raft's term. Each view has a primary (leader).
  • View change: When primary fails, replicas perform view change to elect new primary
  • Normal operation: Primary orders requests, sends to backups
sequenceDiagram participant C as Client participant P as Primary participant B1 as Backup 1 participant B2 as Backup 2 C->>P: Request Note over P: Assign op number P->>B1: PREPARE(view, op, request) P->>B2: PREPARE(view, op, request) B1->>P: PREPARE_OK B2->>P: PREPARE_OK Note over P: Quorum reached P->>B1: COMMIT(op) P->>B2: COMMIT(op) P->>C: Reply

VR vs Raft

  • Similarities: Strong primary, log replication, view changes
  • Differences:
    • VR allows non-primary to initiate view change (Raft uses randomized timeouts)
    • VR commit protocol is slightly different (explicit COMMIT message)
    • VR has more complex recovery protocol
Interview Signal: "What's the difference between VR and Raft?" → Both use primary-backup with views/terms. VR predates Raft by 26 years! Raft's main contribution was understandability and completeness of specification.

ZAB (ZooKeeper Atomic Broadcast)

The consensus protocol used by Apache ZooKeeper. Optimized for ZooKeeper's workload (reads >> writes).

Phases

  1. Leader Election: Fast Leader Election (FLE) algorithm
  2. Discovery: New leader discovers most recent state from followers
  3. Synchronization: Leader syncs followers to its state
  4. Broadcast: Normal operation, atomic broadcast of writes
graph TD A[Leader Election] --> B[Discovery] B --> C[Synchronization] C --> D[Broadcast] D --> A style A fill:#e74c3c style B fill:#f39c12 style C fill:#f39c12 style D fill:#2ecc71

Transaction IDs (zxid)

ZAB uses 64-bit zxid = (epoch, counter):

  • Epoch: Leader election round (high 32 bits)
  • Counter: Sequential transaction counter (low 32 bits)
zxid = (epoch << 32) | counter Example: zxid = 0x0000000300000005 epoch = 3 counter = 5

Broadcast Protocol

Leader proposes transactions using two-phase commit:

sequenceDiagram participant C as Client participant L as Leader participant F1 as Follower 1 participant F2 as Follower 2 C->>L: Write request Note over L: Create proposal
zxid=0x0300000001 L->>F1: PROPOSE(zxid, data) L->>F2: PROPOSE(zxid, data) F1->>L: ACK(zxid) F2->>L: ACK(zxid) Note over L: Quorum ACKed L->>F1: COMMIT(zxid) L->>F2: COMMIT(zxid) L->>C: OK

Ordering Guarantees

  • Reliable delivery: If message m is delivered by one server, it will be delivered by all servers
  • Total order: All servers deliver messages in the same order
  • Causal order: If m1 → m2 (causally), m1 delivered before m2
  • Prefix property: If m is the i-th message delivered, all messages before i are also delivered

ZAB vs Raft

Aspect ZAB Raft
Primary use case Coordination service (ZooKeeper) General-purpose replicated log
Epochs Explicit in zxid (high 32 bits) Term numbers
Discovery phase Separate phase after election Implicit in leader election restrictions
Commit protocol Explicit COMMIT messages Implicit via commitIndex
Reads Can read from followers (with sync) Read from leader or with ReadIndex
Interview Signal: "Why does ZooKeeper use ZAB instead of Raft?" → ZAB predates Raft (2007 vs 2014). ZAB's design is optimized for ZooKeeper's workload (many reads, coordination primitives). Both provide similar guarantees.

Leader Election Patterns

Leader election is a common distributed systems problem with many solutions depending on assumptions.

Bully Algorithm

Simple algorithm assuming processes have unique IDs and can communicate with all others.

Algorithm

  1. Process P notices leader failure
  2. P sends ELECTION message to all processes with higher IDs
  3. If no response, P becomes leader and announces
  4. If response received, P waits for new leader announcement
  5. Higher-ID processes repeat the process
sequenceDiagram participant P1 as P1 (ID=1) participant P2 as P2 (ID=2) participant P3 as P3 (ID=3) participant P4 as P4 (ID=4) participant P5 as P5 (ID=5, crashed) Note over P2: Detects P5 failure P2->>P3: ELECTION P2->>P4: ELECTION P2->>P5: ELECTION (timeout) P3->>P2: OK (I have higher ID) P4->>P2: OK (I have higher ID) P3->>P4: ELECTION P3->>P5: ELECTION (timeout) P4->>P3: OK P4->>P5: ELECTION (timeout) Note over P4: No higher IDs respond
Become leader P4->>P1: COORDINATOR P4->>P2: COORDINATOR P4->>P3: COORDINATOR

Pros: Simple, highest ID always wins
Cons: O(N²) messages, not Byzantine-tolerant, assumes reliable failure detection

Ring Algorithm

Processes arranged in logical ring. Election message circulates, collecting IDs.

graph LR P1((P1)) --> P2((P2)) P2 --> P3((P3)) P3 --> P4((P4)) P4 --> P5((P5)) P5 --> P1 style P3 fill:#e74c3c style P5 fill:#2ecc71

Algorithm

  1. Process P detects leader failure, sends ELECTION message with its ID
  2. Each process adds its ID to the list and forwards
  3. When message returns to initiator, it selects the highest ID and announces

Pros: O(N) messages
Cons: Single point of failure (ring break), slow if ring is large

Raft Leader Election (Randomized Timeouts)

Already covered in detail. Key innovation: randomized election timeouts prevent split votes.

ZooKeeper-Based Leader Election

Use ZooKeeper's ephemeral sequential znodes for leader election.

Algorithm

  1. Each process creates ephemeral sequential znode (e.g., /election/member_0000000001)
  2. Process with lowest sequence number is leader
  3. Others watch the znode immediately before theirs
  4. If watched znode disappears, check if now the leader
graph TD Z[ZooKeeper /election] Z --> M1[member_0000000001
Leader] Z --> M2[member_0000000002
watches M1] Z --> M3[member_0000000003
watches M2] style M1 fill:#2ecc71

Pros: Simple, uses ZooKeeper's guarantees, avoids herd effect
Cons: Requires ZooKeeper (external dependency)

Consensus-Based Leader Election

Use consensus protocol (Paxos, Raft) to agree on leader. This is how Raft's leader election works internally.

Comparison

Algorithm Messages Assumptions Use Case
Bully O(N²) Reliable failure detection, synchronous Small clusters, academic
Ring O(N) Ring topology Academic
Raft O(N) Majority available Production (etcd, Consul)
ZooKeeper O(1) ZooKeeper available Application-level LE
Paxos O(N) Majority available Production (Chubby)

State Machine Replication (SMR)

Fundamental technique for building fault-tolerant services. Idea: replicate a deterministic state machine across multiple servers.

Concept

If multiple state machines:

  • Start in the same initial state
  • Apply the same sequence of deterministic commands
  • Then they will reach the same final state
graph TD subgraph "Initial State" S1[x=0, y=0] S2[x=0, y=0] S3[x=0, y=0] end subgraph "Commands" C1[1: x = x + 1] C2[2: y = x * 2] C3[3: x = y + 1] end subgraph "Final State" F1[x=3, y=2] F2[x=3, y=2] F3[x=3, y=2] end S1 --> C1 S2 --> C1 S3 --> C1 C1 --> C2 C2 --> C3 C3 --> F1 C3 --> F2 C3 --> F3 style F1 fill:#2ecc71 style F2 fill:#2ecc71 style F3 fill:#2ecc71

Requirements

  1. Determinism: Same input → same output (no randomness, no reading wall clock)
  2. Total order: All replicas apply commands in the same order
  3. Consensus: Use consensus protocol to agree on command order

Architecture

graph LR C[Client] --> L[Leader] subgraph "Consensus Layer" L --> F1[Follower 1] L --> F2[Follower 2] end subgraph "State Machines" L --> SM_L[State Machine L] F1 --> SM_F1[State Machine F1] F2 --> SM_F2[State Machine F2] end SM_L --> R[Response to Client] style L fill:#2ecc71

Handling Non-Determinism

Real-world operations are often non-deterministic. Solutions:

  • Leader dictates: Leader computes non-deterministic values (timestamps, random numbers) and includes in log entry
  • Deterministic transformations: Convert non-deterministic operations to deterministic (e.g., "get current time" → "time is 12:00:00.123")
  • External state: Store non-deterministic data (e.g., file contents) in log entry
# Non-deterministic: Bad def process_request(): timestamp = time.now() # Different on each replica! return f"Processed at {timestamp}" # Deterministic: Good def process_request(timestamp): # Leader provides timestamp return f"Processed at {timestamp}"

Examples

  • etcd: Key-value store replicated via Raft SMR
  • ZooKeeper: Coordination service using ZAB SMR
  • Chubby: Google's lock service using Paxos SMR
  • CockroachDB: Each range is a Raft SMR
Interview Signal: "How does etcd achieve fault tolerance?" → State machine replication. Each etcd instance runs the same state machine. Raft ensures all instances apply operations in the same order, so they stay consistent.

Failure Detectors

Mechanisms for detecting when a node has failed. Critical for consensus protocols in partially synchronous systems.

Perfect vs Eventually Perfect

Property Perfect Failure Detector Eventually Perfect FD
Strong Completeness Every crashed process is eventually detected Every crashed process is eventually detected
Strong Accuracy No correct process is ever suspected Eventually, no correct process is suspected
Achievable? Only in synchronous systems Yes, in partially synchronous systems

Heartbeat-Based Detection

Most common approach: send periodic heartbeats, suspect node if timeout expires.

sequenceDiagram participant M as Monitor participant T as Target Note over M: Last heartbeat: t=0 T->>M: Heartbeat (t=1s) T->>M: Heartbeat (t=2s) T->>M: Heartbeat (t=3s) Note over T: Crashed Note over M: t=4s, no heartbeat Note over M: t=5s, no heartbeat Note over M: Timeout (5s)
Suspect T failed

Challenges

  • False positives: Timeout too short → suspect slow nodes
  • False negatives: Timeout too long → slow to detect real failures
  • Network variability: Delays vary unpredictably

Phi Accrual Failure Detector

Adaptive failure detector that outputs a suspicion level (φ) instead of binary alive/dead. Used in Cassandra, Akka.

How It Works

  1. Record heartbeat arrival times
  2. Compute mean (μ) and std deviation (σ) of inter-arrival times
  3. When new heartbeat expected, compute φ based on how late it is:
    • φ = -log₁₀(P(now - last_heartbeat | μ, σ))
  4. Higher φ → higher suspicion
graph LR A[Heartbeat History] --> B[Compute μ, σ] B --> C[Current time - last heartbeat] C --> D["φ = -log₁₀(P(t | μ, σ))"] D --> E{φ > threshold?} E -->|Yes| F[Suspect Failed] E -->|No| G[Alive] style F fill:#e74c3c style G fill:#2ecc71

Advantages

  • Adaptive: Adjusts to network conditions
  • Tunable: Set threshold based on application needs
  • Gradual: Suspicion increases gradually (no sudden flip)
# Example φ values φ = 1 # 90% confidence node is dead φ = 2 # 99% confidence φ = 3 # 99.9% confidence # Cassandra uses threshold φ=8 by default (99.99999%)

Timeout Tuning

Tradeoff between false positives and detection speed:

graph TD A[Short Timeout] --> B[Fast Detection] A --> C[Many False Positives] D[Long Timeout] --> E[Slow Detection] D --> F[Few False Positives] style C fill:#e74c3c style E fill:#f39c12

Best practices:

  • Set timeout > 99th percentile of network RTT
  • Use adaptive detectors (Phi Accrual) for variable networks
  • Monitor false positive rate
  • Increase timeout during known network issues
Interview Signal: "How does Cassandra detect node failures?" → Phi Accrual Failure Detector. Tracks heartbeat history, adapts to network conditions, outputs suspicion level. More robust than fixed timeouts.

Impossibility Results

Theoretical limits on what distributed systems can achieve. These results constrain practical system design.

FLP Impossibility

Fischer, Lynch, Paterson (1985): No deterministic consensus protocol can tolerate even one crash failure in an asynchronous system.

Assumptions

  • Asynchronous: No bounds on message delays or processing time
  • Deterministic: No randomization
  • Crash faults: At most one process can crash

Proof Sketch

FLP shows there exists an execution where the protocol never terminates (consensus is not achieved), because:

  1. An adversary can delay messages such that the system is always in a "bivalent" state (could go either way)
  2. No process can distinguish between a crashed process and a very slow one
  3. Therefore, no deterministic protocol can guarantee termination
graph TD A[Initial State
Bivalent] --> B[Deliver message to P1] A --> C[Deliver message to P2] B --> D[Still Bivalent] C --> E[Still Bivalent] D --> F[Adversary keeps system bivalent forever] E --> F style A fill:#f39c12 style D fill:#f39c12 style E fill:#f39c12 style F fill:#e74c3c

Circumventing FLP

Practical systems work despite FLP because they relax assumptions:

  • Randomization: Ben-Or's randomized consensus, Byzantine agreement
  • Partial synchrony: Assume synchrony holds eventually (Paxos, Raft, ZAB)
  • Failure detectors: Eventually perfect FD allows consensus
  • Weaker guarantees: Eventual consistency, CRDTs (no consensus needed)
Misconception: "FLP means consensus is impossible in practice."
Reality: FLP applies to purely asynchronous systems. Real networks are partially synchronous (synchronous most of the time), allowing Paxos/Raft/ZAB to work.

CAP Theorem (Impossibility Angle)

Revisiting CAP from a theoretical perspective:

Theorem: In the presence of a network partition (P), you must choose between Consistency (C) and Availability (A).

Proof Sketch

  1. Assume system is CA (consistent and available during partition)
  2. Network partitions into two groups: G1 and G2
  3. Client writes v1 to G1 (must succeed for availability)
  4. Client reads from G2 (must return latest value for consistency)
  5. But G2 never received v1 (partition!) → Contradiction
graph TD subgraph "G1" N1[Node 1] N2[Node 2] end subgraph "G2" N3[Node 3] N4[Node 4] end C1[Client] -->|Write v1| N1 C2[Client] -->|Read?| N3 N1 -.-|Partition| N3 N2 -.-|Partition| N4 style N1 fill:#2ecc71 style N3 fill:#e74c3c

What CAP Really Means

  • Not binary: Spectrum of consistency and availability during partitions
  • Partitions are rare: When no partition, can be CA
  • Tunable: Choose consistency level per operation (Cassandra: QUORUM, ONE, ALL)

Other Impossibility Results

  • Two Generals Problem: No protocol for agreement over unreliable channels
  • Consensus lower bound: Ω(N²) messages required for Byzantine agreement in worst case
  • Atomic commit impossibility: No non-blocking atomic commit protocol (2PC blocks on coordinator failure)
Interview Signal: "Why do we use Paxos/Raft if FLP says consensus is impossible?" → FLP applies to purely asynchronous systems. Real systems assume partial synchrony (synchrony holds eventually). Paxos/Raft exploit this assumption and guarantee safety always, liveness eventually.

Consistency & Replication

Replication improves availability and performance but introduces consistency challenges. Multiple replication strategies exist.

Primary-Backup Replication

One primary handles all writes; backups replicate. Simple but primary is a bottleneck.

graph LR C[Client] -->|Write| P[Primary] P -->|Replicate| B1[Backup 1] P -->|Replicate| B2[Backup 2] C2[Client] -.->|Read| B1 C3[Client] -.->|Read| B2 style P fill:#2ecc71

Variants:

  • Synchronous: Wait for all backups to ack (strong consistency, low availability)
  • Asynchronous: Don't wait for backups (eventual consistency, high availability)
  • Semi-synchronous: Wait for k backups (MySQL semi-sync, Kafka min.insync.replicas)

Chain Replication

Nodes arranged in a chain. Writes go through head → tail, reads from tail. Used in CRAQ, Azure Storage.

graph LR C[Client Write] --> H[Head] H --> M[Middle] M --> T[Tail] T --> C2[Client Read] style H fill:#3498db style T fill:#2ecc71

Advantages

  • Strong consistency: Reads from tail see all committed writes
  • High throughput: Head and tail can process different requests
  • Simple recovery: If head fails, next node becomes head

Disadvantages

  • Latency: Writes must traverse entire chain
  • Single read point: Tail is bottleneck for reads

Quorum-Based Replication

Generalization of majority. Configure read quorum (R) and write quorum (W) such that R + W > N.

Quorum Condition

If R + W > N, then read and write quorums overlap → read sees latest write.

# Example: N=5 replicas W=3, R=3 # R+W=6 > 5, strong consistency W=2, R=4 # R+W=6 > 5, strong consistency, favor writes W=4, R=2 # R+W=6 > 5, strong consistency, favor reads W=2, R=2 # R+W=4 < 5, eventual consistency
graph TD subgraph "Write Quorum W=3" N1[Replica 1] N2[Replica 2] N3[Replica 3] end subgraph "Read Quorum R=3" N3[Replica 3] N4[Replica 4] N5[Replica 5] end Note[Overlap guarantees
read sees latest write] style N3 fill:#2ecc71

Used in: Dynamo, Cassandra, Riak (tunable R/W)

Gossip Protocols (Epidemic Protocols)

Nodes periodically exchange state with random peers. Information spreads like an epidemic.

Basic Gossip

  1. Each node maintains local state
  2. Periodically, select random peer and exchange state
  3. Merge state (resolve conflicts if needed)
sequenceDiagram participant N1 participant N2 participant N3 participant N4 Note over N1: Has update X N1->>N2: Gossip (X) Note over N2: Now has X N2->>N4: Gossip (X) Note over N4: Now has X N1->>N3: Gossip (X) Note over N3: Now has X N3->>N4: Gossip (X) Note over N4: Already has X

Properties

  • Eventual consistency: All nodes eventually receive updates
  • Scalability: O(log N) rounds to reach all nodes
  • Fault tolerance: Works even if some messages are lost
  • Simple: No central coordinator

Used in: Cassandra (anti-entropy), Riak (cluster membership), Consul (node discovery)

Replication Type Consistency Write Latency Read Latency Complexity
Primary-Backup (sync) Strong High Low Low
Primary-Backup (async) Eventual Low Low Low
Chain Strong High (chain traversal) Low (tail only) Medium
Quorum Tunable Medium (W nodes) Medium (R nodes) Medium
Gossip Eventual Low (async) Low Medium
Interview Signal: "How does Cassandra achieve tunable consistency?" → Quorum-based replication with configurable R and W. Client can choose per-operation consistency (ONE, QUORUM, ALL). R+W>N gives strong consistency, R+W≤N gives eventual consistency.

Ordering Guarantees

Distributed systems provide different ordering guarantees depending on their design. Stronger guarantees are more expensive.

Ordering Hierarchy

graph TD A[No Ordering] --> B[FIFO Ordering] B --> C[Causal Ordering] C --> D[Total Order] D --> E[Linearizability] A2[Weakest
Cheapest] --> A E2[Strongest
Most Expensive] --> E style A fill:#e74c3c style E fill:#2ecc71

FIFO Ordering

Messages from the same sender are delivered in the order they were sent. No guarantees across different senders.

sequenceDiagram participant S1 as Sender 1 participant S2 as Sender 2 participant R as Receiver S1->>R: Message A S1->>R: Message B Note over R: A delivered before B ✓ S2->>R: Message X S2->>R: Message Y Note over R: X delivered before Y ✓ Note over R: But A/B and X/Y
can be interleaved arbitrarily

Example: TCP guarantees FIFO ordering within a connection.

Causal Ordering

Respects causality (happens-before relation). If event A causally precedes event B (A → B), then A is delivered before B.

sequenceDiagram participant P1 participant P2 participant P3 P1->>P2: A (I'm hungry) P2->>P3: B (Let's eat at noon) Note over P3: P3 delivers A before B
because A → B (causal) P1->>P3: C (What's for lunch?) Note over P3: C can be delivered anytime
(C || B, concurrent)

Implementation: Vector clocks, causal broadcast algorithms

Example: MongoDB causal consistency, session guarantees

Total Order Broadcast

All processes deliver messages in the same order. Equivalent to consensus.

graph TD M1[Message 1] --> N1[Node 1: 1, 2, 3] M2[Message 2] --> N1 M3[Message 3] --> N1 M1 --> N2[Node 2: 1, 2, 3] M2 --> N2 M3 --> N2 M1 --> N3[Node 3: 1, 2, 3] M2 --> N3 M3 --> N3 style N1 fill:#2ecc71 style N2 fill:#2ecc71 style N3 fill:#2ecc71

Used in: Raft (log replication), ZAB (atomic broadcast), Kafka (within partition)

Total Order vs Causal Order

  • Causal: Respects causality but allows concurrent messages to be delivered in different orders
  • Total: All messages have a global order, even concurrent ones

Kafka's Ordering Guarantees

Kafka provides total order within a partition, but no ordering across partitions.

graph TD subgraph "Partition 0" P0_M1[Message 1
offset=0] --> P0_M2[Message 2
offset=1] P0_M2 --> P0_M3[Message 3
offset=2] end subgraph "Partition 1" P1_M1[Message A
offset=0] --> P1_M2[Message B
offset=1] end Note[Messages in different partitions
can be consumed in any order] style P0_M3 fill:#2ecc71 style P1_M2 fill:#2ecc71

Key Takeaways

  • Messages with same key go to same partition (total order)
  • Consumer sees messages in offset order within partition
  • No ordering guarantees across partitions

Linearizability

Strongest consistency model. Operations appear to execute instantaneously at some point between their invocation and response (real-time ordering).

Formal definition: There exists a total order of operations such that:

  1. Respects real-time ordering (if op A completes before op B starts, A comes before B)
  2. Each read returns the value written by the most recent write in the total order
sequenceDiagram participant C1 as Client 1 participant C2 as Client 2 participant DB as Database C1->>DB: Write(x=1) DB->>C1: OK Note over C1,C2: Real-time boundary:
Write completes before read starts C2->>DB: Read(x) DB->>C2: x=1 (must return 1, not 0)
Misconception: "Serializability and linearizability are the same."
Reality: Serializability is about transactions (equivalent to some serial execution). Linearizability is about single operations (respects real-time ordering). Can have serializability without linearizability (snapshot isolation) and vice versa.

Ordering Guarantees Comparison

Guarantee Definition Cost Examples
FIFO Per-sender order preserved Low TCP
Causal Respects happens-before Medium MongoDB causal, CRDTs
Total Order All nodes see same order High (consensus) Raft, ZAB, Kafka (per partition)
Linearizable Real-time total order Highest Spanner, etcd, ZooKeeper
Interview Signal: "Why doesn't Kafka guarantee ordering across partitions?" → Coordination across partitions would require consensus for every batch, killing performance. Kafka's design prioritizes throughput. If you need total order, use a single partition (but loses parallelism).

Real-World Examples

etcd: Linearizable Reads

etcd provides linearizable reads by default (every read goes through Raft). For better performance, can use serializable reads (read from any replica, may be stale).

Cassandra: Eventual Consistency

By default, no ordering guarantees. Can configure quorum reads/writes for stronger consistency.

Spanner: External Consistency

Stronger than linearizability—uses TrueTime to provide external consistency (if txn T1 commits before txn T2 starts in real time, T1's timestamp < T2's timestamp).

MongoDB: Causal Consistency

Client sessions provide causal consistency—reads see all writes that causally precede them.

Advanced Consensus & Membership Protocols

EPaxos (Egalitarian Paxos)

EPaxos is a leaderless consensus protocol that eliminates the single leader bottleneck of Raft and Multi-Paxos. In EPaxos, ANY replica can propose a command without routing through a designated leader, making it optimal for geo-distributed systems where the "closest" replica should handle the request.

The key innovation is the fast path vs slow path decision: If a command does not conflict with other concurrent commands (e.g., writes to different keys), it commits in just 1 RTT (one round trip). If conflicts are detected, EPaxos falls back to a slow path requiring an extra coordination round—but this is still faster than routing all requests through a remote leader in a different datacenter.

Trade-offs: EPaxos is significantly more complex than Raft (harder to implement and debug). It shines in geo-distributed deployments where leader bottlenecks dominate, but offers little advantage in single-datacenter setups where Raft's simplicity wins. CockroachDB's design was influenced by EPaxos, though it uses Raft in practice.

Protocol Leader? Best-Case Latency Complexity Geo-Distributed?
Raft Single leader 2 RTTs (client → leader → followers → client) Low ❌ Leader bottleneck
Multi-Paxos Stable leader 2 RTTs (similar to Raft) Medium ❌ Leader bottleneck
EPaxos Leaderless 1 RTT (no conflicts) High ✅ Optimal for geo-replication
Interview Signal: "Why not always use EPaxos?" → Complexity. Raft is easier to understand, debug, and reason about. EPaxos is only worth it if you have geo-distributed replicas AND your workload has low conflict rates.

SWIM Protocol (Scalable Membership)

Traditional membership protocols use all-to-all heartbeats: every node pings every other node, resulting in O(N²) network overhead. SWIM (Scalable Weakly-consistent Infection-style Process Group Membership) solves this with a clever combination of random probing + indirect probing + gossip dissemination.

How SWIM works: Each node randomly selects K peers per protocol period and sends a ping. If a node doesn't respond, the prober asks K' other nodes to send indirect pings (maybe the direct path is broken, but the node is alive). If all indirect pings fail, the node is marked "suspect" and this suspicion is disseminated via piggybacked gossip on subsequent pings. If no refutation arrives within a timeout, the node is declared failed.

Properties: SWIM achieves constant load per node (O(1) messages sent per period), fast failure detection (~2 protocol periods), and O(log N) convergence time for membership changes to propagate. It is robust to packet loss (indirect probing provides redundancy) and scales to thousands of nodes.

Protocol Network Overhead Failure Detection Time Scalability Used By
All-to-All Heartbeats O(N²) 1 heartbeat interval ❌ Poor (<100 nodes) Simple systems
Gossip-Only O(N log N) Slow (many rounds) ✅ Good (1000s nodes) Cassandra
SWIM O(N) ~2 protocol periods ✅ Excellent (1000s nodes) Consul (Serf), Uber Ringpop
Interview Signal: "How does Consul detect failures so quickly?" → Consul uses SWIM (via Serf library). Mention random probing + indirect probing + gossip dissemination for full credit.

Network Partition Testing (Jepsen)

Jepsen is a testing framework created by Kyle Kingsbury (aphyr) that exposes distributed systems bugs by injecting network partitions and verifying consistency guarantees. The methodology: start a cluster, run concurrent client operations, inject network partitions (via iptables), and then check if the database violated its claimed guarantees (e.g., linearizability, serializability).

Famous Jepsen findings: Redis Sentinel lost acknowledged writes during partitions. MongoDB violated read-your-writes consistency. Elasticsearch lost acknowledged documents. VoltDB exhibited split-brain. The key insight: most databases that CLAIM strong consistency actually violate it under partitions. Many systems pass tests under normal conditions but fail catastrophically when the network misbehaves.

How to test your own system: Use tools like Toxiproxy (inject latency/partitions at TCP level), Chaos Mesh (Kubernetes-native chaos engineering), or Linux tc (traffic control). Write tests that inject faults during writes, then verify that reads respect your consistency model. For linearizability testing, use a checker that builds a partial order of operations and verifies it's consistent with a total order that respects real-time ordering.

Tool Purpose Target Use Case
Jepsen Consistency verification Databases, coordination services Test if system violates claimed guarantees
Toxiproxy Network fault injection Any TCP service Inject latency, packet loss, partitions
Chaos Mesh Kubernetes chaos engineering K8s pods/services Network/pod/DNS faults in K8s
Linux tc Traffic shaping Linux network interfaces Delay, drop, reorder packets
Interview Signal: "Mentioning Jepsen shows you know that distributed guarantees must be TESTED, not just claimed." If asked how to test linearizability, mention: concurrent operations + partition injection + checking that operation history is consistent with a total order respecting real-time.

Merkle DAGs

A Merkle DAG (Directed Acyclic Graph) is a data structure where each node is content-addressed—its identifier is the cryptographic hash of its content plus references to child nodes. This makes the entire structure tamper-evident: any change to any node changes all ancestor hashes up to the root.

Git: Commits form a Merkle DAG where each commit is the hash of its tree (file snapshots) plus parent commit(s). This enables O(1) integrity verification: compare root hashes; if equal, entire histories are identical. IPFS: Files are split into content-addressed blocks linked via Merkle DAG, enabling deduplication (same block = same hash) and distributed retrieval (fetch blocks from any peer). Blockchain: Block headers form a hash chain (special case of Merkle DAG) where each block references the previous block's hash.

System design use case: Efficient sync between replicas. Instead of transferring entire datasets, compare root hashes. If they differ, traverse the DAG to find the divergence point and sync only the differing subtrees. This is how Git efficiently determines what objects to send during push/pull, and how IPFS replicates content across peers.

System Merkle DAG Structure Key Property Benefit
Git Commits → trees → blobs Content-addressed commits Integrity + efficient push/pull
IPFS Files → blocks (chunked) Content-addressed blocks Deduplication + P2P retrieval
Blockchain Block headers (hash chain) Tamper-evident history Trustless verification
Distributed DB Sync Data shards → Merkle DAG Compare root hashes Efficient sync (only diff subtrees)
Interview Signal: "How would you efficiently sync two replicas of a large dataset?" → Merkle DAG (or Merkle tree for simpler cases). Compare root hashes; if equal, done. If not, recursively compare child hashes to find divergence, then sync only differing subtrees.

Conclusion

Distributed systems theory provides the foundation for building correct, fault-tolerant systems. Key takeaways:

  • Time is tricky: Physical clocks are unreliable; use logical clocks (Lamport, Vector, HLC) for causality
  • Consensus is fundamental: Many problems (leader election, SMR, atomic commit) reduce to consensus
  • Paxos vs Raft: Both solve consensus; Raft is designed for understandability
  • Impossibility results constrain us: FLP says deterministic consensus is impossible in async systems, but partial synchrony allows practical protocols
  • Ordering is a spectrum: Stronger ordering guarantees (linearizability) are more expensive than weaker ones (causal, eventual)
  • Failure detection is imperfect: Adaptive detectors (Phi Accrual) handle network variability better than fixed timeouts
Final Interview Tips:
  • Understand why protocols work (safety/liveness proofs), not just mechanics
  • Know real-world systems (etcd uses Raft, Cassandra uses gossip + quorums, ZooKeeper uses ZAB)
  • Be able to reason about tradeoffs (consistency vs availability, latency vs correctness)
  • Explain impossibility results and how practical systems circumvent them

Further Reading

  • Raft Consensus Algorithm — Interactive visualization
  • "Designing Data-Intensive Applications" by Martin Kleppmann — Chapters 8-9 (Distributed Systems)
  • "Distributed Systems" by Maarten van Steen & Andrew Tanenbaum
  • Leslie Lamport's papers: "Time, Clocks, and the Ordering of Events" (1978), "The Part-Time Parliament" (Paxos, 1998)
  • FLP Paper: "Impossibility of Distributed Consensus with One Faulty Process" (1985)