Hirestack
Hirestack
Chapter 2

Replication

📖 12 min read · 8 sections · May 2026

Why this matters

Replication is the foundation of every high-availability system. Without it, a single server crashing means data loss and downtime. With it, you can survive failures, scale reads, place data near users, and roll out updates without downtime.

Replication is also the topic where interviewers probe hardest. Every "design system X at scale" question routes through replication choices: how do you keep multiple copies in sync? What's the failure mode when one crashes? How does a write get to all replicas?

From first principles: what problem are we solving?

You have data. You want to keep it accessible if any single machine fails. So you keep multiple copies on multiple machines. Easy in concept.

The hard part: keeping the copies in sync. Specifically:

The history of distributed databases is essentially the history of answering these questions in different ways.

Three replication architectures

flowchart LR
    subgraph S1["Single-leader"]
        SC1[Client] -->|writes| SL((Leader))
        SC2[Client] -.reads.-> SR1((Replica))
        SL -->|WAL stream| SR1
        SL -->|WAL stream| SR2((Replica))
    end
    subgraph S2["Multi-leader"]
        MC1[Client] --> ML1((Leader))
        MC2[Client] --> ML2((Leader))
        ML1 ---|"replicate both ways"| ML2
    end
    subgraph S3["Leaderless (Dynamo-style)"]
        LC[Client] -->|"write W=2"| LN1((Node))
        LC -->|"write W=2"| LN2((Node))
        LC --> LN3((Node))
        LC2[Client] -->|"read R=2"| LN2
        LC2 --> LN3
    end

Single-leader (primary/replica)

One node is designated the primary (also called leader, master). All writes go to it. The primary propagates writes to one or more replicas (also called followers, slaves, secondaries). Reads can go to the primary (always fresh) or to replicas (possibly stale).

          Writes →  [Primary]
                       ↓ async or sync replication
                  [Replica 1] [Replica 2] [Replica 3]
          ← Reads can go here too

This is the simplest and most common pattern. Used by:

Multi-leader (multi-master)

Multiple nodes accept writes. Each leader replicates to other leaders.

     Writes ↘    ↙ Writes
          [Leader A] ⇄ [Leader B]
              ↓             ↓
         [Replica]     [Replica]

Used for:

The hard problem: conflict resolution. Both leaders accept a write to row X "at the same time" (concurrent in network terms). Which wins?

Leaderless

All nodes are equal. Clients write to multiple nodes simultaneously and read from multiple nodes.

Client writes to W nodes:  [N1] [N2] [N3] [N4] [N5]
Client reads from R nodes: any R of {N1...N5}
If R + W > N: read and write sets overlap → at least one read sees the latest write

This is the Dynamo model. Used by:

Single-leader in depth — the workhorse

sequenceDiagram
    autonumber
    participant C as Client
    participant L as Leader
    participant R1 as Replica 1
    participant R2 as Replica 2
    C->>L: write(x = 1)
    L->>L: append to WAL
    par async to replicas
        L-->>R1: stream WAL
        L-->>R2: stream WAL
    end
    L-->>C: ack
    R1->>R1: apply
    R2->>R2: apply

Synchronous vs asynchronous replication

The most important configuration knob. Determines latency and durability.

ModeWhat it meansWrite latencyData loss on primary fail
AsyncPrimary acks the client; replication happens in backgroundFastest (just primary's fsync)Can lose recent writes
Semi-sync (MySQL)Primary waits for ≥1 replica to receive the log (not necessarily apply it)+0.5–1 ms intra-DCMostly safe
Sync (Postgres synchronous_commit)Primary waits for ≥1 replica to acknowledge persisting (or applying) the write+1–3 ms intra-DC; much more cross-regionSafe for the one replica
Quorum sync (Spanner-style)Primary waits for ≥k of n replicas to ack+2–5 msSafe up to (n-k) failures
Senior insight: even synchronous replication doesn't fully prevent data loss. Consider: primary writes, sync replica acks, primary acks client, primary dies, sync replica dies before another replica catches up. Now you've lost the data. True zero-data-loss requires k-of-n quorums with k≥2 and tolerance for (n-k) failures. This is why Spanner uses quorum replication across multiple regions.

How replication actually moves data

There are three implementations, in roughly decreasing order of "decoupling" between primary and replica:

Statement-based: replay the SQL statement on the replica. Fragile because of nondeterminism: NOW(), RANDOM(), sequences, triggers can all produce different results on different nodes. Largely abandoned for primary replication; sometimes used for analytics.

Physical / WAL shipping: ship the raw WAL records byte-for-byte. The replica is essentially a bit-by-bit copy of the primary's storage. Postgres physical replication, MySQL binary log in row format (sort of).

Logical (row-based): decode the WAL into row-level change events: "row X in table Y changed from {a:1, b:2} to {a:1, b:3}." Replicate these change events. The replica applies them as regular inserts/updates.

Use physical replication for HA replicas. Use logical replication for everything else — online migrations, version upgrades, feeding analytics pipelines, building search indexes.

Change Data Capture (CDC) — the killer use of logical replication

If you can ship row-level changes from the WAL, you can build downstream pipelines:

Postgres → Debezium (CDC tool) → Kafka → Elasticsearch (search)
                                         → Snowflake (analytics)
                                         → Cache invalidation
                                         → Audit logs

This is the foundation of modern data architectures. The transactional database is the source of truth; CDC feeds every downstream system, eliminating dual-writes and inconsistency.

War story — GitHub's MySQL → Vitess migration
GitHub migrated their MySQL fleets to Vitess (sharded MySQL) using logical replication. The new sharded cluster acted as a logical replica of the old monolithic master. Once caught up, traffic was cut over. Without logical replication, this would have required hours of downtime. Logical replication is the foundation of every modern online-migration story. Read: github.blog/engineering/scaling-githubs-database-infrastructure.

Replication lag and what it means for your application

In async setups, replicas lag behind the primary. The lag matters because it determines what your reads see.

Real numbers:

The four "consistency levels" for users

If you read from replicas, what guarantees does the user see?

GuaranteePlain EnglishHow to implement
Eventual consistency"You might see stale data, but it'll be fresh eventually."Read from any replica
Read-your-writes"After you write X, your reads see X."Pin user to primary for N seconds after a write; or include LSN in read; or read from a "session-pinned" replica
Monotonic reads"Same user never sees time go backward."Pin user to one replica for their session
Consistent prefix"Causally related writes appear in order."Don't shard naively; use causal tracking (vector clocks, LSNs)

Most production: pin a user to the primary briefly after a write. Simple and gets read-your-writes for free.

Failover — where things really break

When the primary fails, you need to promote a replica. This sounds simple but has many failure modes.

The failover sequence

  1. Detection: a sentinel (Patroni, Stolon, AWS RDS failover) heartbeats every primary. After 3-5 missed heartbeats, declare primary dead.
  2. Election: pick the replica with the highest LSN (most recent data). If multiple sentinels disagree, they need consensus (typically Raft).
  3. Promotion: tell the chosen replica "you're now the primary." It opens itself to writes.
  4. Reconfiguration: other replicas need to know to follow the new primary. Service discovery (Consul, etcd, DNS) is updated.
  5. Client retargeting: clients need to find the new primary. Connection poolers (pgbouncer) reconnect.
  6. Old primary handling: if the old primary comes back, it MUST NOT accept writes. It needs to be reset to follow the new primary. This is called fencing.

The split-brain problem

What if the old primary doesn't know it's been deposed? It might still be accepting writes from clients that haven't switched. Now you have two primaries — split-brain. Data divergence ensues.

Prevention:

War story — Roblox's 73-hour outage (Oct 2021)
Roblox went down for 73 hours. Root cause: failure cascade in their Consul cluster (the service-discovery backbone for routing). Consul nodes started failing under load, which caused more failover, which caused more load, which caused more failure. Failover was the mechanism that turned a partial outage into a total one. Read: blog.roblox.com/2022/01/roblox-return-to-service-10-28-10-31-2021. Lesson: failover is meant to handle failures; under cascading conditions it can amplify them.

Multi-leader: when and how

When you actually need it

Conflict resolution strategies

Last-writer-wins (LWW): each write carries a timestamp; conflicts resolved by picking the later one. Simple but lossy — clock skew or near-simultaneous writes silently drop data. Used by Cassandra.

Multi-value (siblings): keep both versions; the application reads both and resolves. Used by Riak. Pushes complexity to the app.

CRDTs (Conflict-free Replicated Data Types): data structures with the mathematical property that concurrent updates always merge correctly. Examples: counters (PN-Counter), sets (OR-Set), maps. Used by Redis Enterprise CRDB, Automerge, Riak.

Application-defined resolution: app provides a merge function. Used in git-like systems where the user can review conflicts.

Conflict avoidance via partitioning: assign ownership of each key to one leader. Effectively single-leader-per-key. Most "multi-master" deployments in practice.

Senior take: Most "we need multi-master" requirements can be satisfied by single-leader-per-shard with the shard's primary in the right region. True multi-master with concurrent writes to the same key is the last resort because conflict resolution is hard. Spanner is the rare system that gives you both: distributed transactions across regions with serializability, paid for with the cost of TrueTime and large quorums.

Leaderless / quorum: the Dynamo model

How quorums work

You have N replicas. You write to W of them and consider the write successful. You read from R of them. If W + R > N, your read and write sets overlap by at least one replica — that replica saw the latest write.

N = 3 replicas
W = 2, R = 2  →  W+R = 4 > 3  →  reads see latest writes
W = 1, R = 1  →  W+R = 2 ≤ 3  →  reads may see stale writes
W = 3, R = 1  →  writes are slow but reads are fast
W = 1, R = 3  →  writes are fast but reads are slow

Tuning gives you a knob between availability (smaller W and R make the system more available under partition) and consistency (larger W and R bring stronger guarantees).

The crucial caveat

Quorum overlap alone doesn't give you linearizability — it just gives you "at least one replica with the latest write." If you read R replicas and they have different values, how do you know which is latest? You need:

So Cassandra's "quorum consistency" gives you eventual consistency with high probability of seeing recent writes — not true strong consistency.

Further reading — replication

Interview drills

Q: Walk me through a Postgres primary failover step-by-step.
Patroni heartbeats every replica every ~1 second by writing to etcd/Consul. If primary stops heartbeating for 3-5 cycles, declare it dead. Patroni then queries each replica's LSN (replication position); picks the one with the highest LSN (most recent data). Executes pg_ctl promote on that replica — it now accepts writes. Patroni updates etcd to record the new primary. Other replicas notice the change and start replicating from the new primary. Connection poolers (pgbouncer) get the new endpoint from service discovery. If the old primary returns, Patroni fences it (kills processes via SSH or cloud API), then rebases it to follow the new primary.
Cross-Q: What if two replicas have the same LSN?
A: Pick by deterministic tie-break (lowest node ID). Or, if you suspect data divergence, refuse to auto-failover and require manual intervention.
Cross-Q: What's the failover RTO and RPO in this setup?
A: RTO (recovery time): 5-10 seconds for detection + promotion + client retargeting. RPO (recovery point): depends on async/sync. Async: up to the lag — can lose seconds of writes. Sync: ideally zero, but as noted, multi-failure scenarios can still cause loss.
Q: Your application requires read-your-writes consistency, but you want to use read replicas to scale. Design.
Three approaches. (1) Primary pinning: after a user writes, route that user's reads to the primary for N seconds (typically 5-30). Simple; loses read-replica capacity for active users. (2) LSN-tracking: store the LSN of the user's last write in their session cookie. On read, route to a replica that's caught up to that LSN. Postgres replicas expose their LSN; route to one >= the user's LSN. Falls back to primary if no replica is caught up. (3) Read-through cache: writes update both DB and a per-user cache; reads check cache first. Avoids the staleness window for the user's own data.
Cross-Q: What's the operational cost of LSN tracking?
A: Each read query includes "WHERE pg_last_wal_replay_lsn() >= $session_lsn" or you manage routing in middleware. Adds a microsecond per query, plus session-storage cost. For most applications, (1) is simpler and good enough.
Q: What's the difference between Postgres physical replication and logical replication?
Physical: replicate raw WAL records byte-for-byte. Replica is a bit-identical copy. Fastest, simplest. But: same major version required; same data layout; cannot do schema changes; cannot replicate selectively. Logical: decode WAL into row-change events, replicate as logical inserts/updates. Can cross major versions (huge for zero-downtime upgrades). Can selectively replicate tables. Can feed into CDC pipelines (Debezium → Kafka → Elasticsearch). Slower per row. Doesn't replicate DDL by default.
Cross-Q: When would you use logical over physical for HA?
A: Usually never — physical is enough for HA and is more battle-tested. Logical is for analytics, search indexes, CDC, and major-version upgrades. The simpler rule: physical for replicas you'll fail over to; logical for everything else.
Q: Design replication for a multi-region SaaS where each region handles its own users.
Per-tenant sharding by region. Each shard has its primary in the region where the tenant lives. Other regions have async read replicas for global access if needed. Avoids multi-master; avoids cross-region writes for tenant traffic. Single shard's failover stays within its primary region. Cross-region writes only for the rare "user moves" event, which is a controlled operation.
Cross-Q: What if a user moves regions?
A: Background migration: dual-write to old and new shard during transition; verify data; cut over reads; stop writing to old shard. Like a mini online resharding for one tenant. Usually rare enough to be a manual ops operation.

Red flags