?

Day 0242 minPreface+60 XP

Replication Trade-offs.

Yesterday, you learned how to split data across machines. Today, you learn how to copy it.

5 stages11 code11 Qs

watch it first · newThe whole lesson in a 19-minute animated lecture5 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Preface · 1 min

Preface#

Yesterday, you learned how to split data across machines. Today, you learn how to copy it.

Replication seems simple: keep copies of data on multiple machines. But the moment you have copies, you face the hardest questions in distributed systems:

  • Which copy is the "truth"?
  • How long can copies be out of sync?
  • What happens when the machine holding the truth dies?

By the end of this session, you won't just know what replication is—you'll understand exactly what you're trading with every replication decision, and you'll be able to design systems that handle these trade-offs explicitly.

Let's begin.

ExplainerThe whole lesson: replication trade-offs

Replicas buy availability (one 99.9% server is down ~8.7 hours a year, two ~31 seconds), read scaling (five replicas serve 50,000 reads a second, but writes do not scale) and nearby reads (under 10 ms in Tokyo instead of ~55 ms each way to Virginia), at the price of waiting for coordination or serving stale data. Leader-follower replication can be synchronous (no loss, slow followers stall writes), asynchronous (fast, lagging, unreplicated writes lost) or semi-synchronous; multi-leader must resolve conflicts, and leaderless quorums see the latest write when W + R > N. Lag makes data go backwards or replies appear before messages, so the consistency level is chosen per use case; in the URL shortener a friend gets a 404 for abc123 from a replica ~50 ms behind, fixed by a synchronous replica, primary reads for new URLs or a write-through cache, with failover after 10 to 30 seconds and regional read replicas for ~10 ms redirects.

Part I · Stage 1 of 5 · 12 min

Part I: Foundations#

Chapter 1: Why Replication Matters#

Your turn · checkpoint 1not answered

Estimate first: what does a second copy buy?

One database server is up 99.9% of the time. About how many hours a year is it down? Now run two servers, either of which can serve requests, and assume they fail independently. What's the combined uptime, and how much downtime is left?

Think it through, or jot it down. Then compare.

One strong answer

  • One server: 0.1% of a year's ~8,760 hours ≈ 8.8 hours down.
  • Two servers: both must be down at once, 0.001 × 0.001 = one in a million, so 99.9999%: about 31 seconds a year.
  • Only if the failures are independent: the 31 seconds rests entirely on that assumption.

1.1 The Three Reasons to Replicate#

Every replicated system exists for one or more of these reasons:

Reason 1: High Availability#

If your database server dies, your application dies with it. With replicas, you can failover to another copy and keep serving requests.

The math: A single server with 99.9% uptime has about 8.8 hours of downtime per year. Two servers, either of which can serve requests, have combined uptime of 99.9999%—about 31 seconds of downtime per year (assuming independent failures).

Reason 2: Read Scaling#

A single database might handle 10,000 reads per second. With 5 read replicas, you can handle 50,000 reads per second. Writes still go to one place, but reads spread across all copies.

When this works Read-heavy workloads (10:1 or higher read-to-write ratio). Most web applications qualify.

When this doesn't work Write-heavy workloads. Replication doesn't help with write throughput—every write must go to every replica.

Reason 3: Geographic Distribution#

Users in Tokyo shouldn't wait for a round trip to a server in Virginia. With replicas in multiple regions, users read from nearby copies.

The physics: Light travels ~200km per millisecond in fiber. Tokyo to Virginia is ~11,000km = ~55ms minimum latency each way. A local replica reduces this to <10ms.

1.2 The Fundamental Trade-off#

Here's the truth about replication that every engineer must internalize:

You cannot have instantaneous, consistent replication without sacrificing availability or performance.

This isn't a limitation of current technology. It's physics and mathematics. The CAP theorem (and its extension, PACELC, which covers the latency trade-off when there is no partition) formalizes this, but the intuition is simple:

  • Keeping replicas perfectly in sync requires coordination
  • Coordination takes time (network round trips)
  • During that time, you're either waiting (slower) or serving potentially stale data (inconsistent)

Every replication strategy is a different point on this trade-off spectrum. Your job is to choose the right point for your system.

Chapter 2: Replication Architectures#

There are three fundamental architectures for replication. Each makes different trade-offs.

2.1 Leader-Follower (Primary-Replica)#

How It Works#

One node is the leader (also called primary or master). All writes go to the leader. The leader then sends changes to followers (also called replicas or secondaries). Reads can go to the leader or any follower.

DiagramSingle-leader replication
%% title: Single-leader replication
%% steps:
%% W --> L: Every write goes to the one leader
%% L --> F1, L --> F2, L --> F3: The leader streams each change to all of its followers
%% RD --> F1, RD --> F2, RD --> F3: Reads are spread across the followers, so read capacity grows with each one
flowchart TD
  W@{ icon: "sd:users", label: "Writes" } --> L[("Leader")]:::core
  L -->|"Replication stream"| F1@{ icon: "sd:database", label: "Follower" }
  L -->|"Replication stream"| F2@{ icon: "sd:database", label: "Follower" }
  L -->|"Replication stream"| F3@{ icon: "sd:database", label: "Follower" }
  RD@{ icon: "sd:users", label: "Reads (distributed)" } --> F1
  RD --> F2
  RD --> F3
  class W,RD client
  class F1,F2,F3 store
  1. Every write goes to the one leader
  2. The leader streams each change to all of its followers
  3. Reads are spread across the followers, so read capacity grows with each one
Your turn · checkpoint 2not answered

Should the leader wait?

A client writes to the leader. The leader can acknowledge right away and replicate in the background, or wait until a follower confirms. What does each choice cost when a follower is slow, and when the leader dies a moment after acknowledging? Is there a middle option?

Think it through, or jot it down. Then compare.

One strong answer

  • Wait (synchronous): no acknowledged write is lost if the leader dies, but every write pays a round trip, and a slow or dead follower stalls writes.
  • Don't wait (asynchronous): fast writes, and slow followers don't affect clients, but writes not yet replicated die with the leader.
  • Middle (semi-synchronous): wait for one follower, replicate to the rest in the background. One replica has every committed write, and you only wait for the fastest follower.
Synchronous vs Asynchronous#

Synchronous replication: Leader waits for follower(s) to confirm the write before acknowledging to the client.

DiagramSynchronous replication
%% title: Synchronous replication
sequenceDiagram
  autonumber
  participant C as Client
  participant L as Leader
  participant F as Follower
  C->>L: Write
  L->>F: Replicate
  F-->>L: Confirm
  L-->>C: ACK

Pros:

  • Followers are always up-to-date
  • No data loss on leader failure

Cons:

  • Write latency includes network round-trip to follower
  • If follower is slow or dead, writes stall

Asynchronous replication: Leader acknowledges to client immediately, replicates in the background.

DiagramAsynchronous replication
%% title: Asynchronous replication
sequenceDiagram
  autonumber
  participant C as Client
  participant L as Leader
  participant F as Follower
  C->>L: Write
  L-->>C: ACK
  L-)F: replicate (background)

Pros:

  • Fast writes (no waiting for followers)
  • Followers being slow doesn't affect clients

Cons:

  • Followers may lag behind leader
  • Data loss possible on leader failure (unreplicated writes)
Semi-Synchronous: The Practical Middle Ground#

Many databases offer semi-synchronous replication (PostgreSQL and MySQL replicate asynchronously by default, and it's an option you turn on): wait for at least one follower to confirm, then acknowledge to client. Other followers replicate asynchronously.

DiagramSemi-synchronous replication
%% title: Semi-synchronous replication
sequenceDiagram
  autonumber
  participant C as Client
  participant L as Leader
  participant F1 as Follower 1 (sync)
  participant F2 as Follower 2, 3, ...
  C->>L: Write
  L->>F1: Replicate
  F1-->>L: Confirm
  L-->>C: ACK
  L-)F2: replicate (async)

This gives you:

  • Durability: At least one replica has every committed write
  • Performance: Only wait for the fastest follower
  • Availability: Can tolerate slow followers
When Leader-Follower Breaks#

Failure mode 1: Leader dies

With async replication, some writes may not have reached any follower. Those writes are lost.

With sync replication, a follower can be promoted to leader without data loss. But during failover (typically 10-60 seconds), writes fail.

Failure mode 2: Split brain

Network partition isolates the leader. Followers think it's dead and elect a new leader. Now you have two leaders accepting writes. When the network heals, you have conflicting data.

Failure mode 3: Replication lag

Under heavy load, followers fall behind. A read from a follower returns stale data. In extreme cases, followers can be minutes or hours behind.

2.2 Multi-Leader (Multi-Master)#

How It Works#

Multiple nodes accept writes. Each leader replicates to the others.

DiagramMulti-leader replication
%% title: Multi-leader replication
flowchart TD
  W1@{ icon: "sd:users", label: "Writes" } --> L1[("Leader 1")]:::core
  W2@{ icon: "sd:users", label: "Writes" } --> L2[("Leader 2")]:::core
  L1 <-->|"Replication"| L2
  L1 --> F1@{ icon: "sd:database", label: "Follower" }
  L1 --> F2@{ icon: "sd:database", label: "Follower" }
  L2 --> F1
  L2 --> F2
  class W1,W2 client
  class F1,F2 store
Why Use Multi-Leader?#

Geographic distribution Users in Europe write to the Europe leader; users in US write to the US leader. Each region has low-latency writes.

Datacenter failover If one datacenter goes down, the other continues accepting writes. No failover delay.

The Conflict Problem#

When two leaders accept conflicting writes, what happens?

Scenario: User updates their email on their phone (hits EU leader) and their laptop (hits US leader) at the same time.

Leader Write
EU Leader UPDATE users SET email='new@eu.com' WHERE id=123
US Leader UPDATE users SET email='new@us.com' WHERE id=123

Both succeed locally. When they replicate to each other, which one wins?

Conflict resolution strategies:

  1. Last-write-wins (LWW): Use timestamps; latest write wins. Simple but can lose data if clocks disagree.

  2. Merge: For some data types, merge is possible. CRDTs (Conflict-free Replicated Data Types) are designed for this.

  3. Application-level resolution: Notify the application of the conflict; let it decide.

  4. Avoid conflicts: Design the system so conflicts are rare. For example, each user's data is primarily written in one region.

When Multi-Leader Breaks#

Conflict resolution is hard Most teams underestimate the complexity. LWW causes data loss. Custom resolution requires significant engineering.

Operational complexity Debugging issues across multiple leaders is harder. "Which leader was this written to?" becomes a real question.

Consistency is weak By design, you accept that replicas diverge temporarily. Some applications can't tolerate this.

2.3 Leaderless (Dynamo-style)#

How It Works#

No designated leader. Clients write to multiple nodes directly. Reads also go to multiple nodes, and the client resolves differences.

DiagramLeaderless replication
%% title: Leaderless replication
flowchart TD
  W@{ icon: "sd:users", label: "Write" } --> N1@{ icon: "sd:database", label: "Node1" }
  W --> N2@{ icon: "sd:database", label: "Node2" }
  W --> N3@{ icon: "sd:database", label: "Node3" }
  N1 --- RQ@{ icon: "sd:users", label: "Read (quorum)" }
  N2 --- RQ
  N3 --- RQ
  class W,RQ client
  class N1,N2,N3 store
Quorums: The Math of Consistency#

With N replicas, you configure:

  • W: Number of nodes that must confirm a write
  • R: Number of nodes that must respond to a read

If W + R > N, the read and write sets overlap in at least one node, so a read normally sees the latest write. This is not an absolute guarantee: sloppy quorums, concurrent writes, and partially failed writes can still return stale values, and a quorum read is not necessarily linearizable.

Example with N=3:

  • W=2, R=2: Read and write quorums overlap → reads normally see the latest write (strong-ish consistency)
  • W=1, R=1: No overlap → eventual consistency, but faster
Quorum overlap (N=3, W=2, R=2)
  • Nodes: [A] [B] [C]
  • Write (W=2): Write to A — ok, B — ok, C pending
  • Read (R=2): Read from B — ok, C — ok

B was in both → B has the latest write → read sees it

Sloppy Quorums and Hinted Handoff#

What if nodes are unavailable? Strict quorums would fail the request. Sloppy quorums allow writing to any available nodes, with hinted handoff—the temporary node holds the write and forwards it when the proper node recovers.

This improves availability but weakens consistency guarantees.

When Leaderless Breaks#

Clock skew Many leaderless systems use timestamps for conflict resolution. Clock skew can cause newer writes to be overwritten by older ones.

Complexity Clients must handle quorum logic, conflict resolution, and read repair. The complexity shifts to the client or a thick client library.

Tail latency Waiting for multiple nodes (quorum) means your latency is set by the slowest node in the quorum, not the average.

Chapter 3: Replication Lag and Its Consequences#

3.1 What Is Replication Lag?#

In asynchronous replication, followers don't have the latest data immediately. The delay between a write on the leader and that write appearing on a follower is replication lag.

Normal conditions Milliseconds to low seconds.

Under load Can grow to minutes or longer.

Failure conditions If a follower is disconnected, lag is infinite until it reconnects.

3.2 Anomalies Caused by Replication Lag#

Anomaly 1: Read Your Own Writes#

User submits a form. The write goes to the leader. User's next request reads from a follower that hasn't received the write yet. User sees the old data and thinks their submission failed.

Read your own writes
  • Time 0 —
    User writes "new email" → Leader
  • Time 1 —
    User reads profile → Follower (lag: hasn't received write yet)
  • Time 2 —
    User sees old email, confused
  • Time 3 —
    Follower receives write
  • Time 4 —
    User refreshes, sees new email, more confused

Solutions:

  • Read from leader for recently-written data
  • Track write timestamps; only read from followers caught up to that point
  • Sticky sessions: route user to the same follower consistently (only helps if that follower has caught up to the user's last write)
ExplainerReplication lag and read-your-writes

A write lands on the leader and reaches the follower a moment later. A read routed to the lagging follower returns the old value; routing the user's reads to the leader for recently written data (read-your-writes) shows the new value at once.

Anomaly 2: Monotonic Reads#

User makes two read requests. First hits follower A (caught up). Second hits follower B (lagging). User sees data go "backwards in time."

Monotonic reads
  • Read 1 —
    → Follower A: Returns version 5
  • Read 2 —
    → Follower B: Returns version 3

User: "Where did my data go?"

Solution: Sticky sessions or reading from the same replica within a session.

Anomaly 3: Consistent Prefix Reads#

Two causally related writes (A, then B) that land on different partitions may appear in a different order on a follower (within a single leader's log, order is preserved, so this shows up in partitioned databases).

Consistent prefix reads
Node Events
Leaders (different partitions) Write A (9:00), Write B (9:01)
Follower 1 Receives B (9:02), Receives A (9:03)

Read from Follower 1 at 9:02:30:

  • Sees B but not A
  • B depends on A (e.g., "Reply to message A")
  • User sees reply to non-existent message

Solution: Ensure causally related writes go through the same partition/leader, or use logical clocks.

3.3 Measuring and Monitoring Lag#

Every replicated system should expose:

  1. Replication lag in seconds/bytes: How far behind is each follower?
  2. Lag histogram: Distribution of lag across followers
  3. Lag alerts: Page when lag exceeds acceptable threshold
sql11 lines
-- PostgreSQL: Check replication lag
SELECT 
  client_addr,
  state,
  sent_lsn,
  write_lsn,
  flush_lsn,
  replay_lsn,
  pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
  replay_lag
FROM pg_stat_replication;

Chapter 4: Consistency Models#

Different applications need different consistency guarantees. Choosing the wrong one means either poor performance (too strong) or confusing bugs (too weak).

4.1 Strong Consistency#

Every read returns the most recent write. All clients see the same data at the same time.

How to achieve it Synchronous replication, or read from leader only.

Cost Higher latency (coordination required), lower availability (leader failure blocks reads).

When you need it:

  • Financial transactions
  • Inventory counts (avoid overselling)
  • Any case where stale reads cause real harm

4.2 Eventual Consistency#

If no new writes occur, all replicas will eventually converge to the same value. No guarantee about when.

How to achieve it Asynchronous replication with no special handling.

Cost Clients may see stale data, out-of-order updates, or temporary inconsistencies.

When it's acceptable:

  • Social media feeds (seeing a post 30 seconds late is fine)
  • Analytics dashboards (approximate numbers are acceptable)
  • Caches (by definition, allowed to be stale)

4.3 Read-Your-Writes Consistency#

A user always sees their own writes immediately. Other users may see stale data.

How to achieve it:

  • Route user's reads to the leader for recently-written data
  • Track user's last write timestamp; only read from caught-up replicas

When you need it:

  • User profiles (user updates settings, expects to see them)
  • Document editing (author's changes should appear immediately)
  • Any self-referential read after write

4.4 Monotonic Reads#

Once a user sees a version of data, they never see an older version on subsequent reads.

How to achieve it: Sticky sessions (same replica per user session).

When you need it:

  • Conversation threads (messages shouldn't disappear and reappear)
  • Activity timelines (events shouldn't reorder)

4.5 Causal Consistency#

If event A causes event B, everyone sees A before B. Concurrent events may be seen in different orders.

How to achieve it: Logical clocks (vector clocks, Lamport timestamps) to track causality.

When you need it:

  • Messaging (reply should appear after original message)
  • Collaborative editing (operations that depend on each other must apply in order)
Your turn · checkpoint 3not answered

Pick a consistency level for each

Choose strong, read-your-writes, causal or eventual for: a bank balance, users viewing their own profile, chat messages, a view counter, URL shortener redirects, URL shortener creates.

Think it through, or jot it down. Then compare.

One strong answer

  • Bank balance: strong. A wrong balance is a real money problem.
  • Own profile: read-your-writes. Users expect to see their own change; others can wait.
  • Chat: causal. A reply must appear after the message it answers.
  • View counter: eventual. Approximate is fine.
  • Redirects: eventual (a second of staleness is fine). Creates: strong, because the URL must exist before it's shared.

4.6 Choosing Your Consistency Level#

Use Case Recommended Level Why
Bank balance Strong Incorrect balance = real money problems
E-commerce inventory Strong Overselling costs money and trust
Social media feed Eventual Late posts don't cause harm
User profile (to user) Read-your-writes User expects to see their changes
User profile (to others) Eventual Others can wait a bit
Chat messages Causal Replies must follow messages
View counter Eventual Approximate is fine
URL shortener reads Eventual 1-second stale is fine
URL shortener writes Strong URL must exist before sharing
Quick check4 questions · +5 XP each

Replication architectures and lag

  1. Q1/4 Why is semi-synchronous replication a practical middle ground?

    Answer & explanation

    Answer: B

    Waiting for one follower gives durability without stalling on slow followers; the rest catch up asynchronously. Other followers can still lag, so semi-sync does not make every replica current.

  2. Q2/4 A leaderless store has N = 5 replicas. Which setting guarantees every read overlaps the latest write?

    Answer & explanation

    Answer: C

    You need W + R > N. 3 + 3 = 6 > 5, so at least one node in every read quorum has the write. W = 3, R = 2 and W = 1, R = 4 both sum to exactly 5, which leaves room for a read that misses the write.

  3. Q3/4 A user refreshes a thread twice. The first read shows version 5, the second shows version 3. What fixes this anomaly?

    Answer & explanation

    Answer: A

    This is a monotonic-reads violation: two reads hit replicas with different lag. Reading from the same replica within a session prevents going back in time. More replicas or more leaders add more chances for divergent lag, not fewer.

  4. Q4/4 A follower shows a reply ("Reply to message A") but not message A itself, because it received the writes out of order. Which guarantee was violated?

    Answer & explanation

    Answer: A

    Causally related writes must be seen in order. The fix is routing related writes through the same partition or leader, or tracking causality with logical clocks. Read-your-writes is about seeing your own writes; here the problem is the order of two writes.

Part II · Stage 2 of 5 · 7 min

Part II: The Design Challenge#

Chapter 5: Extending the URL Shortener with Replication#

Yesterday, you designed a partitioned URL shortener. Today, we add replication for availability and read scaling.

5.1 Current State (From Day 1)#

DiagramCurrent state (from Day 1)
%% title: Current state (from Day 1)
flowchart TD
  APP["App Servers"]:::core --> C@{ icon: "tech:redis", label: "Cache<br/>(Redis)" }
  C --> P0@{ icon: "sd:database", label: "Partition 0" }
  C --> P1@{ icon: "sd:database", label: "Partition 1" }
  C --> P2@{ icon: "sd:database", label: "Partition 2" }
  class C cache
  class P0,P1,P2 store

Each partition is a single PostgreSQL instance. If Partition 1 dies, all URLs mapped to it are unavailable.

5.2 Adding Read Replicas#

For each partition, we add read replicas:

DiagramRead replicas per partition
%% title: Read replicas per partition
flowchart TD
  C@{ icon: "sd:cache", label: "Cache" }
  C --> P0[("Partition 0<br/>Primary")]:::core
  C --> P1[("Partition 1<br/>Primary")]:::core
  C --> P2[("Partition 2<br/>Primary")]:::core
  P0 --> P0R0@{ icon: "sd:database", label: "Replica 0" }
  P0 --> P0R1@{ icon: "sd:database", label: "Replica 1" }
  P1 --> P1R0@{ icon: "sd:database", label: "Replica 0" }
  P1 --> P1R1@{ icon: "sd:database", label: "Replica 1" }
  P2 --> P2R0@{ icon: "sd:database", label: "Replica 0" }
  P2 --> P2R1@{ icon: "sd:database", label: "Replica 1" }
  class C cache
  class P0R0,P0R1,P1R0,P1R1,P2R0,P2R1 store

5.3 Write Path#

Writes always go to the primary:

pythoncreate_short_url · 15 lines
def create_short_url(short_code: str, original_url: str) -> URLMapping:
    partition = get_partition(short_code)
    primary = get_primary_connection(partition)
    
    result = primary.execute(
        "INSERT INTO urls (short_code, original_url, created_at) "
        "VALUES (%s, %s, NOW()) RETURNING created_at",
        (short_code, original_url)
    )
    
    return URLMapping(
        short_code=short_code,
        original_url=original_url,
        created_at=result.created_at
    )

5.4 Read Path#

Reads can go to any replica:

pythonredirect · 20 lines
def redirect(short_code: str) -> Optional[str]:
    # Try cache first
    cached = cache.get(f"url:{short_code}")
    if cached:
        return cached
    
    # Cache miss - read from replica
    partition = get_partition(short_code)
    replica = get_random_replica(partition)  # Load balance across replicas
    
    result = replica.execute(
        "SELECT original_url FROM urls WHERE short_code = %s",
        (short_code,)
    )
    
    if result:
        cache.setex(f"url:{short_code}", 3600, result.original_url)
        return result.original_url
    
    return None
Your turn · checkpoint 4not answered

The friend who clicked too fast

Writes go to the partition's primary, and redirects read from a random replica that runs ~50 ms behind. A user creates abc123, shares it, and a friend clicks 3 ms later on a cache miss. What does the friend see? Name three fixes and what each costs.

Think it through, or jot it down. Then compare.

One strong answer

  • A 404 for a URL that exists: the replica hasn't received it yet. It's read-your-writes broken, for someone else's write.
  • Synchronous replication (remote_apply): one more round trip per create, and only the synchronous standby is guaranteed fresh.
  • Read new URLs from the primary (a 5-second "just created" flag): more logic, more reads on the primary.
  • Write-through cache at creation: redirects hit the cache, not the replica. Redis joins the write path, which is acceptable because it's already critical.

5.5 The Replication Lag Problem#

Scenario: User creates a short URL and immediately shares it.

The replication lag problem
  • 0.000s —
    User creates short URL "abc123" → Written to Primary 1
  • 0.001s —
    Primary 1 acknowledges write to user
  • 0.002s —
    User shares link with friend
  • 0.003s —
    Friend clicks link → Request routed to Replica 1
  • 0.004s —
    Replica 1 queries for "abc123" → NOT FOUND (replica lag ~50ms)
  • 0.005s —
    Friend sees 404 error
  • 0.050s —
    Replica 1 receives replication of "abc123"
  • 0.100s —
    Friend retries → Works

The friend saw a 404 for a URL that exists. This is a read-your-writes violation—but for someone else's write.

WorksheetHow many URLs the lag exposes (rates and timings are assumptions)
Value
Creates (assumed, the rate Solution 1 below uses) 1,000/s
Partitions (Day 1's diagram) 3 → ~330 creates/s per primary
Async replica lag (the timeline above) ~50 ms
New URLs a replica doesn't have yet, at any moment 1,000 × 0.05 s = ~50
Acknowledged URLs lost if one primary dies (async) ~330 × 0.05 s ≈ ~17
Failover window with writes failing (assumed 30 s, within 10–60 s) ~330 × 30 ≈ ~10,000 creates fail and must be retried
Synchronous replication (assumed 1 ms in-region round trip) +1 ms on every create

Conclusion: the stale window is tiny, about 50 URLs at any moment, but those are exactly the URLs people are sharing right now, so the fix should target new URLs. A dead primary costs far more through its failover window than through lost writes.

5.6 Solutions for the Stale Read Problem#

Solution 1: Synchronous Replication#

Configure PostgreSQL for synchronous replication. Writes don't acknowledge until at least one replica confirms.

sql5 lines
-- postgresql.conf on primary
-- 'on' only waits for the standby to flush WAL (durability);
-- 'remote_apply' waits until the change is visible to queries on the standby
synchronous_commit = remote_apply
synchronous_standby_names = 'replica1'

Trade-off: Every write is slower by one network round-trip to the replica (plus apply time with remote_apply). For a URL shortener doing 1,000 creates/second, adding 1ms per write might be acceptable. Note that only the synchronous standby is guaranteed fresh—reads that land on async replicas can still miss the URL.

Solution 2: Read from Primary for New URLs#

After creating a URL, read from primary for a short window:

pythoncreate_short_url · 28 lines
def create_short_url(short_code: str, original_url: str) -> URLMapping:
    result = create_in_primary(short_code, original_url)
    
    # Mark this URL as "just created" for 5 seconds
    cache.setex(f"new:{short_code}", 5, "1")
    
    # Also cache the URL itself
    cache.setex(f"url:{short_code}", 3600, original_url)
    
    return result

def redirect(short_code: str) -> Optional[str]:
    cached = cache.get(f"url:{short_code}")
    if cached:
        return cached
    
    partition = get_partition(short_code)
    
    # Check if this is a newly created URL
    if cache.exists(f"new:{short_code}"):
        # Read from primary to ensure we see it
        db = get_primary_connection(partition)
    else:
        # Safe to read from replica
        db = get_random_replica(partition)
    
    result = db.execute(...)
    ...

Trade-off: Slightly more complex logic. Primary gets more reads for new URLs. But new URLs are also likely to be hot (just shared), so they'd probably end up cached quickly anyway.

Solution 3: Write-Through Cache#

Cache the URL at creation time, not at first read:

pythoncreate_short_url · 7 lines
def create_short_url(short_code: str, original_url: str) -> URLMapping:
    result = create_in_primary(short_code, original_url)
    
    # Immediately cache - redirects will hit cache, not database
    cache.setex(f"url:{short_code}", 3600, original_url)
    
    return result

Trade-off: The cache is now in the write path. If Redis is slow or down, URL creation is affected. But for a URL shortener, this is probably acceptable since Redis is already critical infrastructure.

ExplainerBreak it: replication lag vs stale reads

Sliders for the write rate, replica lag and the number of rows being read (10,000 by default, the lab's assumption), starting at the lesson's 1,000 creates/s and ~50 ms of lag. Model: reads are spread evenly over those rows and a read is stale when its row was written less than one lag ago, so the stale share = writes/s × lag ÷ rows (0.5% here). Reading URLs created in the last 5 s from the primary removes stale reads until the lag passes 5 s.

5.7 Failover Design#

What happens when a primary dies?

Automatic Failover#

Use a tool like Patroni (for PostgreSQL) or orchestration built into your database:

  1. Primary stops responding to heartbeats
  2. Followers detect primary is gone (timeout: 10-30 seconds)
  3. Followers elect a new primary (consensus among remaining nodes)
  4. Connection poolers (PgBouncer) are updated to point to new primary
  5. Old primary, if it recovers, joins as a follower
Failover
  1. Before failover — Primary → Replica1, Replica2
  2. Primary dies
  3. After failover — Replica1 (promoted) → Replica2
    • Old Primary (if recovered, joins as replica)
Handling Writes During Failover#

During the failover window (10-60 seconds), writes fail. Options:

  1. Return errors: Tell users "please retry." Simple but bad UX.

  2. Queue writes: Accept writes into a queue, apply when new primary is elected. Complex, risk of inconsistency.

  3. Fast failover: Optimize detection and promotion to minimize window. Best practical approach.

For a URL shortener, option 1 is probably fine. Users can retry creating a URL. It's not like a payment that might double-charge.

Handling Reads During Failover#

If the primary dies but replicas are healthy, reads continue to work (assuming you don't require reading from primary).

This is a major benefit of read replicas: read availability during primary failures.

Chapter 6: Replication in Multi-Region Deployments#

6.1 The Latency Problem#

Your URL shortener is successful. Users are global. But all infrastructure is in us-east-1.

User Route Round trip
User in Tokyo → us-east-1 150ms
User in London → us-east-1 80ms
User in São Paulo → us-east-1 120ms

For redirects (the hot path), this latency is painful. Users expect near-instant redirects.

6.2 Read Replicas in Multiple Regions#

Deploy read replicas in each major region:

DiagramRegional read replicas
%% title: Regional read replicas
flowchart TD
  P[("us-east-1<br/>Primary Cluster")]:::core
  P --> EU@{ icon: "sd:database", label: "eu-west-1<br/>Read Replicas" }
  P --> AP@{ icon: "sd:database", label: "ap-northeast-1<br/>Read Replicas" }
  P --> SA@{ icon: "sd:database", label: "sa-east-1<br/>Read Replicas" }
  EU --- EUU@{ icon: "sd:users", label: "Europe" }
  AP --- APU@{ icon: "sd:users", label: "Tokyo" }
  SA --- SAU@{ icon: "sd:users", label: "São Paulo" }
  class EU,AP,SA store
  class EUU,APU,SAU client

Reads: Served from local region (~10ms) Writes: Go to us-east-1 (~80-150ms)

6.3 Dealing with Cross-Region Replication Lag#

Cross-region replication has higher lag than in-region (50-200ms typically).

The problem intensifies: User in Tokyo creates URL, shares with friend in Tokyo. Friend clicks. Local Tokyo replica doesn't have the URL yet.

Solutions:

  1. Write-through to local cache: When URL is created, synchronously populate the Tokyo cache.

  2. Route creator's reads to primary region: For a few seconds after creation, route that user's reads to us-east-1.

  3. Accept the edge case: For a URL shortener, the friend can retry in 500ms. It's not ideal but might be acceptable.

6.4 Multi-Region Active-Active#

For write-heavy workloads or true global availability, you might want active-active:

DiagramActive-active regions
%% title: Active-active regions
flowchart LR
  US[("us-east-1<br/>Primary<br/>(Americas)")]:::core <-->|"Bi-dir Repl."| EU[("eu-west-1<br/>Primary<br/>(Europe)")]:::core

Users in Americas write to us-east-1. Users in Europe write to eu-west-1. Changes replicate both directions.

The conflict problem: Same URL created in both regions simultaneously? With a URL shortener, this is unlikely (short codes are random), but you'd need conflict resolution if it happens.

Simpler alternative for URL shorteners: Keep single-primary, accept the write latency. URL creation is infrequent compared to redirects. A 150ms create is acceptable if redirects are 10ms.

Quick check3 questions · +5 XP each

Replicating the URL shortener

  1. Q1/3 A user creates a short URL and a friend clicks it 3ms later. The read goes to a replica about 50ms behind and returns 404. Which fix does the lesson recommend?

    Answer & explanation

    Answer: C

    With a write-through cache, the friend's redirect hits the cache, not the lagging replica, and Redis is already in the architecture. Reading every redirect from the primary would undo the read scaling replicas were added for.

  2. Q2/3 What new risk does write-through caching add to URL creation?

    Answer & explanation

    Answer: B

    Caching at creation time makes Redis part of every write. The lesson accepts this because Redis is already critical infrastructure. First-access misses are what write-through avoids; the database stays the source of truth.

  3. Q3/3 Users are global, and the primary is in us-east-1 with read replicas in other regions. Why keep a single primary instead of going active-active?

    Answer & explanation

    Answer: B

    The hot path is reads, which local replicas already serve in about 10ms. Active-active only helps writes, at the cost of conflict resolution. Cross-region lag still exists with one primary, which is why the lesson also discusses local write-through caching.

Part III · Stage 3 of 5 · 4 min

Part III: Advanced Topics#

Chapter 7: Replication Internals#

Understanding how replication works under the hood helps you debug issues and make informed decisions.

7.1 Write-Ahead Log (WAL) Replication#

Most databases (PostgreSQL, MySQL, MongoDB) use log-based replication:

  1. Every write is first recorded in a sequential log (WAL)
  2. The log is shipped to replicas
  3. Replicas apply the log entries to their data
DiagramWAL shipping
%% title: WAL shipping
flowchart TD
  subgraph PRI["Primary"]
    direction TB
    W["Write"]:::client --> WAL[("WAL")]:::store
    WAL --> AD["Apply to data"]:::core
  end
  subgraph REP["Replica"]
    direction TB
    RW["Receive WAL"]:::service --> RA["Apply to data"]:::service
  end
  WAL -->|"Send to replicas"| RW

Why WAL?

  • Sequential writes are fast (disk optimization)
  • Log captures exact changes (no interpretation needed)
  • Replicas can catch up by replaying log from any point

7.2 Logical vs Physical Replication#

Physical replication Ship the raw WAL bytes. Replica is byte-for-byte identical to primary. Fast, but tied to same database version.

Logical replication Ship logical changes (INSERT/UPDATE/DELETE with values). More flexible, can replicate between different versions or even different databases. Slightly slower.

Type What is shipped
Physical "Write these bytes to page 1234 at offset 567"
Logical "INSERT INTO users (id, name) VALUES (1, 'Alice')"

7.3 Replication Slots and Catchup#

What if a replica goes offline for an hour?

Without replication slots Primary might delete old WAL segments (to reclaim disk). Replica comes back and can't catch up—needs full resync.

With replication slots Primary retains WAL needed by each replica. Replica can always catch up, but disk usage grows if replica is down too long.

sql4 lines
-- Monitor replication slot disk usage
SELECT slot_name, 
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as retained_wal
FROM pg_replication_slots;

Chapter 8: Handling Failover Correctly#

Failover sounds simple: promote a replica when the primary dies. In practice, it's full of edge cases.

Your turn · checkpoint 5not answered

What breaks if the primary is only cut off?

A network partition separates the primary from its replicas, but some clients can still reach it. The replicas see no heartbeats and promote a new primary. What goes wrong, and what stops it?

Think it through, or jot it down. Then compare.

One strong answer

  • Split brain: clients on one side keep writing to the old primary, clients on the other write to the new one, and when the network heals there are two primaries with conflicting data.
  • Fencing stops the old primary from accepting writes: power it off (STONITH), revoke its storage or network access, or make it hold a lease in ZooKeeper or etcd; cut off from the lease store, it can't renew and stops taking writes.

8.1 The Split-Brain Problem#

Network partition isolates the primary:

DiagramNetwork partition isolates the primary
%% title: Network partition isolates the primary
flowchart TD
  P[("Primary<br/>(isolated)")]:::bad x--x|"Network Partition · X X X"| R[("Replicas<br/>+ Application")]:::store
  P --- CA["Clients A"]:::client
  R --- CB["Clients B"]:::client
  • Clients A continue writing to the old primary
  • Replicas can't reach primary, promote a new one
  • Clients B write to the new primary
  • Network heals: two primaries with conflicting data

8.2 Fencing: The Solution to Split-Brain#

Fencing ensures the old primary can't accept writes after failover:

  1. STONITH (Shoot The Other Node In The Head): Literally power off the old primary via IPMI/BMC.

  2. Storage fencing: Revoke the old primary's access to shared storage.

  3. Network fencing: Reconfigure network rules to block the old primary.

  4. Lease-based: Primary must regularly renew a lease (in ZooKeeper/etcd). If it can't reach the lease store, it stops accepting writes.

pythonPrimary · 18 lines
# Lease-based fencing pseudocode
class Primary:
    def __init__(self):
        self.lease = self.acquire_lease()
    
    def accept_write(self, write):
        if not self.lease.is_valid():
            raise Exception("Lost leadership, rejecting write")
        
        self.execute(write)
    
    def background_loop(self):
        while True:
            try:
                self.lease.renew()
            except LeaseRenewalFailed:
                self.demote_to_read_only()
            sleep(lease_ttl / 3)

8.3 Failover Runbook#

A real failover runbook for our URL shortener:

Failover procedure: Partition X Primary Failure
  1. Detect
    • Alert: "Primary X not responding to health checks for 30 seconds"
    • Verify: Check if network issue or actual server failure
  2. Decide
    • If network issue: Do NOT failover, wait for network recovery
    • If server failure: Proceed with failover
  3. Fence the old primary
    • Remove from load balancer
    • If possible, power off or isolate network
  4. Select new primary
    • Choose replica with least replication lag
    • Verify replica is healthy
  5. Promote
    • Run: pg_ctl promote -D /data/postgres
    • Verify: SELECT pg_is_in_recovery(); -- should return false
  6. Reconfigure
    • Update connection strings / PgBouncer
    • Remaining replicas point to new primary
  7. Verify
    • Test write: Create test URL
    • Test read: Query for test URL
    • Monitor replication from new primary to replicas
  8. Cleanup
    • When old primary recovers: reinitialize as replica
    • Post-incident review within 24 hours

Estimated downtime: 30-60 seconds (writes only; reads continue)

Quick check3 questions · +5 XP each

Replication internals and failover

  1. Q1/3 A network partition isolates the primary, the replicas promote a new one, and some clients keep writing to the old primary. What prevents the old primary from accepting those writes?

    Answer & explanation

    Answer: C

    Without fencing you get split brain: two primaries with conflicting data. A lease, STONITH or network fencing guarantees only one primary accepts writes. Picking the least-lagged replica reduces data loss, but it does nothing to stop the old primary.

  2. Q2/3 A replica is offline for an hour and the primary keeps a replication slot for it. What is the trade-off?

    Answer & explanation

    Answer: A

    The slot makes the primary retain every WAL segment the replica still needs. Leave a replica down too long and that disk usage grows without bound. A full resync is what happens without a slot, when the needed WAL has already been deleted.

  3. Q3/3 The failover runbook says to promote the replica with the least replication lag. Why?

    Answer & explanation

    Answer: B

    With async replication, unreplicated writes vanish when the primary dies. The least-lagged replica is missing the fewest of them. Disk space and network position do not change how much data is lost.

Part IV · Stage 4 of 5 · 3 min

Part IV: Discussion and Trade-offs#

Chapter 9: The Hard Questions#

These are the questions you should be asking (and answering) in your design sessions.

9.1 "User creates URL, immediately shares it, friend clicks—what happens with async replication?"#

This is the core replication lag problem. Your answer should include:

  1. Acknowledge the problem: Yes, with async replication, the friend might get a 404.

  2. Quantify the risk: Replication lag is typically <100ms. The friend would have to click within 100ms of the share—unlikely but possible.

  3. Propose mitigations:

    • Write-through cache at creation time (friend hits cache, not replica)
    • Semi-sync replication (wait for one replica before acknowledging)
    • Accept the edge case (friend retries, it works)
  4. Make a recommendation: "For a URL shortener, I'd use write-through cache. It's simple, and we already have Redis in the architecture. The URL is cached before the user even finishes sharing."

9.2 "When would you accept stale reads? When not?"#

Accept stale reads:

  • View counters, like counts (approximate is fine)
  • News feeds (seeing a post 30 seconds late doesn't matter)
  • Product listings (prices don't change every second)
  • Analytics dashboards (real-time accuracy isn't required)

Reject stale reads:

  • Account balances (stale balance → wrong financial decisions)
  • Inventory counts (stale inventory → overselling)
  • Permission checks (stale permissions → security hole)
  • User authentication state (stale logout → security issue)

The URL shortener case:

  • URL resolution: Can be eventually consistent. A URL created 1 second ago can tolerate <100ms lag.
  • URL creation: Must be strongly consistent. User needs confirmation that the URL exists.
  • URL deletion: Tricky. If we delete but replicas still serve it, is that a problem? For most URL shorteners, yes—the user deleted it for a reason.

9.3 "What replication factor would you use?"#

Replication factor = number of copies of each piece of data

Common choices:

  • RF=1: No replication. Don't do this in production.
  • RF=2: Can survive 1 node failure. Common for cost-sensitive workloads.
  • RF=3: Can survive 1 failure with no degradation (quorum still possible). Standard choice.
  • RF=5: Can survive 2 failures. For highly critical data.

For the URL shortener: RF=3 is appropriate. It's standard for production workloads, allows maintenance without risk, and the storage overhead is acceptable.

9.4 "How do you handle a replica that's fallen hours behind?"#

Detection Monitor replication lag. Alert if lag exceeds threshold (e.g., 1 minute).

Diagnosis Why is it behind?

  • Network issue: Fix network, let it catch up.
  • Slow disk: Replica can't apply changes fast enough.
  • Long-running query: Blocking replication on the replica.

Recovery options:

  1. Let it catch up: If the primary is retaining WAL, the replica will eventually catch up.
  2. Remove from rotation: Stop sending reads to this replica until it catches up.
  3. Re-sync from scratch: If too far behind or WAL is gone, rebuild the replica from a backup.

For URL shortener: Remove from read rotation immediately. A replica hours behind is serving stale data. If catching up would take longer than a rebuild (or the WAL it needs is gone), rebuild it instead.

Chapter 10: Session Summary#

What You Should Know Now#

After this session, you should be able to:

  1. Explain leader-follower, multi-leader, and leaderless architectures with trade-offs
  2. Design a replication strategy based on consistency and availability requirements
  3. Identify replication lag anomalies and their solutions
  4. Handle failover scenarios with explicit fencing strategies
  5. Extend a partitioned system with replication (the URL shortener)

Key Trade-offs to Remember#

Decision Trade-off
Sync vs Async replication Consistency vs Write latency
More replicas Better read scaling vs Higher write replication cost
Leader-follower vs Multi-leader Simplicity vs Multi-region write latency
Strong vs Eventual consistency Correctness vs Performance/Availability
Higher replication factor Better durability vs Storage and write cost

Questions to Ask in Every Design#

  1. What consistency level does this data actually need?
  2. What's the acceptable replication lag?
  3. What happens during failover? How long is the outage?
  4. Can readers tolerate stale data? For how long?
  5. What's the strategy for a replica that falls far behind?
Quick check3 questions · +5 XP each

Stale reads, lagging replicas and replication factor

  1. Q1/3 Which read should NOT be served from a replica that may lag by a few seconds?

    Answer & explanation

    Answer: A

    A stale permission lets a revoked user keep acting: a security hole. Like counts and listings tolerate brief staleness. The rule is to accept stale reads where approximate is fine and reject them where staleness causes real harm.

  2. Q2/3 Monitoring shows one URL-shortener replica is two hours behind. What should you do first?

    Answer & explanation

    Answer: C

    A replica hours behind serves very stale data, so stop routing reads to it first. If catching up would take longer than a rebuild, rebuilding from backup is faster. Promoting the most-behind replica would lose all the writes it is missing.

  3. Q3/3 Why is a replication factor of 3 the standard choice over 2?

    Answer & explanation

    Answer: A

    With 3 copies, losing one still leaves 2 of 3, so quorum operations and maintenance continue without degradation. RF=2 does survive one failure, but with no margin. More replicas cost more writes; they do not speed them up.

Part V · Stage 5 of 5 · 10 min

Part V: Interview Questions and Answers#

Chapter 11: Real-World Interview Scenarios#

11.1 Conceptual Questions#

Interview question 1not answered
Question 1: "Explain the difference between synchronous and asynchronous replication."#

Interviewer's Intent: Testing fundamental understanding.

Answer it out loud first. Then compare.

Strong Answer:

"In synchronous replication, the primary waits for at least one replica to confirm the write before acknowledging to the client. This guarantees that if the primary dies, no acknowledged writes are lost. The cost is latency—every write incurs a network round-trip to the replica.

In asynchronous replication, the primary acknowledges immediately and replicates in the background. Writes are faster, but if the primary dies before replicating, those writes are lost.

Many systems offer semi-synchronous replication (PostgreSQL and MySQL default to asynchronous): wait for one replica, replicate to others asynchronously. This balances durability with performance. PostgreSQL with synchronous_commit = on and one sync standby is a common configuration."

Interview question 2not answered
Question 2: "What is replication lag and why does it matter?"#

Interviewer's Intent: Testing understanding of practical implications.

Answer it out loud first. Then compare.

Strong Answer:

"Replication lag is the delay between a write on the primary and that write appearing on a replica. In normal conditions, it's milliseconds. Under load or with network issues, it can grow to seconds or minutes.

It matters because reads from lagging replicas return stale data. This causes anomalies: a user updates their profile, refreshes, and sees the old version because their read hit a lagging replica.

There are specific consistency violations to watch for:

  • Read-your-writes: User doesn't see their own recent write
  • Monotonic reads: User sees data go backwards (fresher replica, then lagging one)
  • Consistent prefix: Causally related writes appear out of order

Mitigations include sticky sessions, reading from primary for recent writes, and write-through caching."

Interview question 3not answered
Question 3: "What is split-brain and how do you prevent it?"#

Interviewer's Intent: Testing understanding of distributed systems failure modes.

Answer it out loud first. Then compare.

Strong Answer:

"Split-brain occurs when a network partition causes two nodes to both believe they're the primary. Each accepts writes, and when the partition heals, you have conflicting data.

Prevention requires fencing—ensuring the old primary can't accept writes after a new primary is elected. Common approaches:

  1. STONITH—literally power off the old primary via out-of-band management
  2. Lease-based leadership—the primary must regularly renew a lease in a consensus system like ZooKeeper. If it can't reach the lease store, it stops accepting writes
  3. Storage fencing—revoke the old primary's access to shared storage

I'd use lease-based fencing for most applications. It's less invasive than STONITH and works well with cloud infrastructure where IPMI access isn't always available."

Interview question 4not answered
Question 4: "Explain the CAP theorem in practical terms."#

Interviewer's Intent: Testing ability to translate theory to practice.

Answer it out loud first. Then compare.

Strong Answer:

"CAP says you can't have Consistency, Availability, and Partition tolerance simultaneously. Since network partitions will happen, you're really choosing between consistency and availability during a partition.

In practice, it's not binary—it's a spectrum. For example:

  • During normal operation, you can have both C and A
  • During a partition, you choose: reject writes (consistent but unavailable) or accept writes (available but potentially inconsistent)

Modern systems let you tune this per operation. DynamoDB lets you choose strongly consistent or eventually consistent reads. Cassandra lets you set quorum levels.

For the URL shortener example: I'd choose availability for reads (serve from any replica, even if stale) but consistency for URL creation (write must be durable before acknowledging). Different operations, different CAP positions."

11.2 Design Questions#

Interview question 5not answered
Question 5: "Design the replication strategy for a global user profile service."#

Interviewer's Intent: Testing end-to-end replication design.

Answer it out loud first. Then compare.

Strong Answer:

"First, let me understand the access patterns. User profiles are read-heavy—users view profiles constantly, update rarely. Reads should be low-latency globally.

Architecture: Leader-follower with regional read replicas.

DiagramLeader-follower with regional read replicas
%% title: Leader-follower with regional read replicas
flowchart TD
  P[("Primary (us-east-1)")]:::core --> EU[("EU Replicas")]:::store
  P --> AS[("Asia Replicas")]:::store
  P --> SA[("SA Replicas")]:::store

Writes go to the primary in us-east-1. Users accept the latency for updates since they're infrequent.

Reads go to the nearest regional replica. Replication is asynchronous—users can tolerate seeing their profile update a few hundred milliseconds late.

For read-your-writes consistency: when a user updates their profile, I'd write-through to a global cache (something like Redis Global Datastore or a CDN cache). The next read hits the cache, not the lagging replica.

For cross-region failover: if us-east-1 goes down, I'd fail over to eu-west-1. This requires promotion of a replica to primary and reconfiguring the other replicas. Failover time would be 30-60 seconds with automation.

Replication factor: 2 replicas in each of the four regions, us-east-1 included (total of 9 nodes including primary). This gives fault tolerance within each region and allows maintenance without risk."

Interview question 6not answered
Question 6: "How would you handle replication for a financial ledger?"#

Interviewer's Intent: Testing understanding of strong consistency requirements.

Answer it out loud first. Then compare.

Strong Answer:

"Financial ledgers are the poster child for strong consistency. An incorrect balance or double-spend is unacceptable. I'd design very differently than the profile service.

Replication strategy: Synchronous replication to at least one replica before acknowledging any write. I'd use a consensus protocol (Raft, Paxos) rather than simple leader-follower to guarantee no data loss during failover.

DiagramSync to one replica, async to the rest
%% title: Sync to one replica, async to the rest
flowchart TD
  W["Write"]:::client --> P[("Primary")]:::core
  P --> S1[("Sync Replica 1")]:::store
  S1 --> ACK(["ACK"]):::ok
  P -.->|"for read scaling"| A[("Async Replica 2, 3")]:::store

Consistency: All reads go to the primary or sync replicas only. I'd never serve a balance from an async replica.

Availability trade-off: During a partition, I'd reject writes rather than risk inconsistency. Financial correctness > availability. Users see an error message, but their money is safe.

Failover: Use consensus-based election. The new leader is guaranteed to have all committed transactions. Fencing is critical—I'd use lease-based fencing with a short TTL (5-10 seconds).

For audit compliance: Keep the replication log (WAL) immutable and archived. This provides a complete history of all transactions for regulatory review.

The key insight: this is exactly the opposite of the profile service. Strong consistency, sacrifice availability, synchronous replication. The requirements drive fundamentally different designs."

11.3 Scenario-Based Questions#

Interview question 7not answered
Question 7: "Your replica is 2 hours behind. Walk me through how you'd handle this."#

Interviewer's Intent: Testing operational skills.

Answer it out loud first. Then compare.

Strong Answer:

"First, I'd remove it from the read rotation immediately. A replica 2 hours behind is serving data that's 2 hours stale—that's unacceptable for most applications.

Then diagnosis:

  1. Check network: Is there a network issue between primary and replica? I'd check packet loss, latency, bandwidth utilization.

  2. Check replica health: Is the replica CPU/IO bound? Is something blocking replication apply (long-running queries, locks)?

  3. Check primary: Is the primary WAL generation unusually high? Maybe there's a bulk load or migration running.

For recovery, it depends on the cause:

  • If it's a temporary issue (network blip), and the primary is retaining WAL, I'd let it catch up. Monitor the lag—it should decrease steadily.

  • If the replica can't keep up (undersized hardware), I'd either upgrade it or rebuild on better hardware.

  • If the primary has already deleted the required WAL (no replication slots), I'd need to rebuild the replica from a fresh backup.

The decision point: if catching up will take longer than rebuilding (say, lag > 24 hours), rebuilding is often faster with modern tooling like pg_basebackup.

Throughout, I'd communicate status to the team and update our incident channel. A degraded replica affects our read capacity and fault tolerance."

Interview question 8not answered
Question 8: "Your primary database just died. Walk me through the failover process."#

Interviewer's Intent: Testing incident response skills.

Answer it out loud first. Then compare.

Strong Answer:

"Assuming we have automated failover configured (and we should), here's what happens and what I'd verify:

Automated steps:

  1. Health checks detect primary is unresponsive (typically 10-30 second timeout)
  2. Failover coordinator (Patroni, RDS, etc.) initiates failover
  3. Sync replica is promoted to primary
  4. Connection poolers/load balancers are updated
  5. Other replicas are reconfigured to follow the new primary

My immediate actions:

  1. Acknowledge the alert, join incident channel
  2. Verify failover completed: Can we write to the new primary?
  3. Check application health: Are services reconnecting successfully?
  4. Monitor for any data issues: Did we lose any writes?

If automated failover didn't trigger:

  1. Manually verify primary is actually down (not just monitoring failure)
  2. Fence the old primary (remove from network/load balancer)
  3. Identify the most up-to-date replica
  4. Promote it manually: pg_ctl promote or equivalent
  5. Update connection configuration
  6. Verify writes work

Post-incident:

  1. Investigate why primary died (hardware? OOM? disk full?)
  2. When old primary is recovered, reinitialize it as a replica
  3. Write up incident report with timeline and learnings
  4. If automated failover failed, fix the automation

Expected timeline: 30-60 seconds for automated failover, 5-15 minutes for manual if automation fails."

Interview question 9not answered
Question 9: "How would you test your replication and failover setup?"#

Interviewer's Intent: Testing proactive reliability thinking.

Answer it out loud first. Then compare.

Strong Answer:

"Testing replication should happen regularly, not just after setup.

Continuous monitoring:

  • Replication lag metrics on all replicas (alert if > threshold)
  • Replica health checks (is the replica applying WAL?)
  • Primary WAL generation rate (unusual spike might indicate issues)

Regular testing (monthly):

  1. Planned failover drill: Deliberately promote a replica, verify applications handle it gracefully, then fail back.
  2. Chaos engineering: Kill the primary unexpectedly (in staging), verify automated failover works.
  3. Lag simulation: Introduce network latency to a replica, verify monitoring detects it and the replica is removed from rotation.

Pre-production validation:

  • Before any replication config change, test in staging
  • Verify both automated and manual failover work
  • Test the scenario where automated failover fails and you need manual intervention

Application-level testing:

  • Verify applications reconnect after failover (connection string changes, timeouts)
  • Test that applications handle brief write unavailability gracefully
  • Verify no data corruption after failover

Documentation:

  • Runbook for manual failover should be tested annually by someone who wasn't involved in writing it
  • If they can't follow it, the runbook is broken

The goal is confidence: when a real failover happens at 3 AM, you know it will work because you've tested it repeatedly."

11.4 Deep-Dive Questions#

Interview question 10not answered
Question 10: "Compare replication in PostgreSQL, MySQL, and MongoDB."#

Interviewer's Intent: Testing breadth of knowledge.

Answer it out loud first. Then compare.

Strong Answer:

"PostgreSQL uses streaming replication by default. WAL segments are shipped to replicas over a persistent connection. It supports synchronous replication (wait for N replicas), replication slots (ensure WAL is retained for slow replicas), and logical replication (for cross-version or selective replication).

Strengths: Very robust, battle-tested. Synchronous replication is straightforward. Logical replication allows flexible topologies. Weaknesses: Failover isn't built-in—you need external tools like Patroni or Stolon.

MySQL uses binary log replication. Similar concept to WAL, but MySQL has more replication modes: statement-based (replicates SQL), row-based (replicates actual data changes), or mixed. MySQL Group Replication adds consensus-based replication for automatic failover.

Strengths: Flexible replication formats. Group Replication provides built-in automatic failover. Weaknesses: Historically more complex to configure correctly. GTID (Global Transaction IDs) make it better, but legacy setups can be tricky.

MongoDB uses replica sets: a primary and multiple secondaries with automatic failover via a Raft-based election protocol. Writes go to primary; reads can go to secondaries with various read preferences.

Strengths: Built-in automatic failover, no external tools needed. Read preference is configurable per query. Weaknesses: Write concern configuration can be confusing. Before MongoDB 5.0, the default write concern (w:1) prioritized availability over durability; 5.0+ defaults to majority (except when arbiters leave no data-bearing majority, where it stays w:1).

For our URL shortener, I'd likely use PostgreSQL with Patroni for automatic failover. It's what I know best, and the tooling is mature. But MongoDB's built-in replica sets would also work well—simpler to set up, automatic failover out of the box."

Interview question 11not answered
Question 11: "What is quorum-based replication and when would you use it?"#

Interviewer's Intent: Testing understanding of consistency mechanisms.

Answer it out loud first. Then compare.

Strong Answer:

"Quorum replication requires a minimum number of nodes to agree before considering an operation complete. With N nodes, you configure W (write quorum) and R (read quorum). If W + R > N, a read normally sees the latest write because at least one node participates in both (strict quorums only; sloppy quorums and concurrent writes can still return stale data).

Example with N=5:

  • W=3, R=3: Majority for both reads and writes. Strong consistency.
  • W=1, R=5: Fast writes, expensive reads. All nodes must respond to read.
  • W=5, R=1: Expensive writes (all nodes), fast reads (any single node).

Cassandra and Riak (following the original Dynamo paper) use this model. Common settings:

  • QUORUM (W=majority, R=majority): Balanced consistency/performance
  • ONE (W=1, R=1): Fastest, but eventual consistency
  • ALL (W=all, R=all): Strongest, but any node failure blocks operations

When to use it:

  • When you need tunable consistency per operation
  • When you want availability during partial failures (quorum can succeed without all nodes)
  • For distributed databases without a single leader

Trade-offs:

  • Higher latency (waiting for multiple nodes)
  • Tail latency is the slowest node in the quorum
  • More complex client logic

For the URL shortener, I probably wouldn't use quorum-based replication. Leader-follower is simpler and sufficient. But if I were building a globally distributed key-value store where each region needs to accept writes, quorum-based approaches like Dynamo-style replication would be appropriate."

Chapter 12: Interview Preparation Checklist#

Before your interview, make sure you can:

Concepts#

Concepts0/4 ready
  • Explain sync vs async replication and their trade-offs
  • Describe leader-follower, multi-leader, and leaderless architectures
  • Define replication lag and its anomalies (read-your-writes, monotonic reads)
  • Explain split-brain and fencing mechanisms

Design Skills#

Design Skills0/4 ready
  • Add replication to a partitioned system design
  • Choose consistency levels based on requirements
  • Design for multi-region deployment
  • Handle failover scenarios explicitly

Operational Knowledge#

Operational Knowledge0/4 ready
  • Walk through a failover runbook
  • Diagnose and resolve a lagging replica
  • Test replication and failover procedures
  • Monitor replication health

Real Systems#

Real Systems0/3 ready
  • Know replication in PostgreSQL, MySQL, or MongoDB (at least one well)
  • Understand quorum-based systems (Cassandra, Riak, Dynamo-style stores)
  • Cite real-world examples (how does X company handle this?)

Exercises · 1 min

Exercises#

Exercise 1: Consistency Level Selection#

For each scenario, choose the appropriate consistency level and justify:

  1. Twitter-like feed: User posts a tweet, expects to see it immediately in their own feed
  2. Inventory count: E-commerce site showing "Only 3 left in stock!"
  3. Like counter: Showing "1.2M likes" on a viral post
  4. Bank transfer: Moving money between accounts
  5. Password change: User resets password, tries to log in immediately

Exercise 2: Failover Design#

Design the failover procedure for a three-node PostgreSQL cluster (1 primary, 2 replicas) running a payment processing system. Consider:

  • Detection time
  • Fencing mechanism
  • Promotion criteria
  • Application impact
  • Data loss risk

Exercise 3: Replication Lag Mitigation#

Your application has a flow: user submits a form, you save to the database, then redirect to a "success" page that reads the data. Users are occasionally seeing "not found" errors. Design three different solutions with trade-offs for each.

Further Reading#

  • Docs

    "Designing Data-Intensive Applications" Chapter 5: Replication (the definitive reference)

  • Read

    Jepsen.io: Kyle Kingsbury's analyses of database consistency under failure

  • Read

    GitHub's MySQL High Availability: How they handle replication and failover

  • Read

    Stripe's Online Migrations: Maintaining consistency during schema changes

  • Blog

    Patroni Documentation: Automatic failover for PostgreSQL

Appendix · 4 min

Appendix: Code Reference#

A.1 Replication Lag Monitoring#

pythonReplicaStatus · 50 lines
from dataclasses import dataclass
from typing import List, Optional
import time

@dataclass
class ReplicaStatus:
    hostname: str
    lag_bytes: int
    lag_seconds: float
    state: str  # 'streaming', 'catchup', 'disconnected'
    
class ReplicationMonitor:
    def __init__(self, primary_conn, alert_threshold_seconds: float = 30):
        self.primary_conn = primary_conn
        self.alert_threshold = alert_threshold_seconds
    
    def get_replica_status(self) -> List[ReplicaStatus]:
        """Query primary for replication status."""
        query = """
            SELECT 
                client_addr as hostname,
                pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) as lag_bytes,
                COALESCE(EXTRACT(EPOCH FROM replay_lag), 0) as lag_seconds,
                state
            FROM pg_stat_replication
        """
        results = self.primary_conn.execute(query)
        
        return [
            ReplicaStatus(
                hostname=r['hostname'],
                lag_bytes=r['lag_bytes'],
                lag_seconds=r['lag_seconds'],
                state=r['state']
            )
            for r in results
        ]
    
    def check_health(self) -> List[str]:
        """Return list of alerts for unhealthy replicas."""
        alerts = []
        replicas = self.get_replica_status()
        
        for replica in replicas:
            if replica.state != 'streaming':
                alerts.append(f"{replica.hostname}: State is {replica.state}, not streaming")
            elif replica.lag_seconds > self.alert_threshold:
                alerts.append(f"{replica.hostname}: Lag is {replica.lag_seconds:.1f}s (threshold: {self.alert_threshold}s)")
        
        return alerts

A.2 Read-Your-Writes Consistency Helper#

pythonReadYourWritesRouter · 77 lines
import time
from typing import Optional
import redis

class ReadYourWritesRouter:
    """
    Routes reads to primary for recently-written keys,
    otherwise to replicas.
    """
    
    def __init__(
        self, 
        primary_conn,
        replica_pool,
        cache: redis.Redis,
        stickiness_seconds: float = 5.0
    ):
        self.primary = primary_conn
        self.replicas = replica_pool
        self.cache = cache
        self.stickiness_seconds = stickiness_seconds
    
    def record_write(self, key: str):
        """Call this after every write to track the key."""
        # Store timestamp of write
        self.cache.setex(
            f"written:{key}",
            int(self.stickiness_seconds),
            str(time.time())
        )
    
    def get_connection_for_read(self, key: str):
        """
        Returns primary connection if key was recently written,
        otherwise returns a replica connection.
        """
        written_at = self.cache.get(f"written:{key}")
        
        if written_at:
            # Recently written - use primary
            return self.primary
        else:
            # Safe to use replica
            return self.replicas.get_random()
    
    def read(self, key: str, query: str, params: tuple) -> Optional[dict]:
        """Execute a read with appropriate routing."""
        conn = self.get_connection_for_read(key)
        return conn.execute(query, params)
    
    def write(self, key: str, query: str, params: tuple) -> dict:
        """Execute a write and track it."""
        result = self.primary.execute(query, params)
        self.record_write(key)
        return result


# Usage example
router = ReadYourWritesRouter(primary, replica_pool, redis_client)

# Write - goes to primary, tracked for 5 seconds
router.write(
    key="user:123",
    query="UPDATE users SET email = %s WHERE id = %s",
    params=("new@example.com", 123)
)

# Immediate read - routed to primary because of recent write
result = router.read(
    key="user:123",
    query="SELECT * FROM users WHERE id = %s",
    params=(123,)
)

# Read after 5 seconds - safe to go to replica
time.sleep(5)
result = router.read(...)  # Goes to replica

A.3 Automatic Failover Detection#

pythonNodeState · 90 lines
import time
import threading
from enum import Enum
from typing import Callable, Optional

class NodeState(Enum):
    PRIMARY = "primary"
    REPLICA = "replica"
    FAILED = "failed"
    UNKNOWN = "unknown"

class FailoverDetector:
    """
    Monitors primary health and triggers failover callback
    when primary is determined to be down.
    """
    
    def __init__(
        self,
        primary_conn,
        health_check_interval: float = 1.0,
        failure_threshold: int = 3,
        on_failover: Optional[Callable] = None
    ):
        self.primary = primary_conn
        self.interval = health_check_interval
        self.threshold = failure_threshold
        self.on_failover = on_failover
        
        self.consecutive_failures = 0
        self.state = NodeState.UNKNOWN
        self._running = False
        self._thread = None
    
    def health_check(self) -> bool:
        """Returns True if primary is healthy."""
        try:
            result = self.primary.execute("SELECT 1", timeout=1.0)
            return result is not None
        except Exception:
            return False
    
    def _monitor_loop(self):
        while self._running:
            healthy = self.health_check()
            
            if healthy:
                self.consecutive_failures = 0
                self.state = NodeState.PRIMARY
            else:
                self.consecutive_failures += 1
                
                if self.consecutive_failures >= self.threshold:
                    self.state = NodeState.FAILED
                    
                    if self.on_failover:
                        self.on_failover()
                    
                    # Reset after triggering
                    self.consecutive_failures = 0
            
            time.sleep(self.interval)
    
    def start(self):
        self._running = True
        self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
        self._thread.start()
    
    def stop(self):
        self._running = False
        if self._thread:
            self._thread.join()


# Usage
def handle_failover():
    print("Primary failed! Initiating failover...")
    # 1. Fence old primary
    # 2. Promote replica
    # 3. Update connection strings
    # 4. Alert on-call

detector = FailoverDetector(
    primary_conn=primary,
    health_check_interval=1.0,
    failure_threshold=3,
    on_failover=handle_failover
)

detector.start()

this lesson in shorts · 2

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Day 02 · Replication Trade-offs
Finish Day 2Stages clear as you read them (0/5 so far). Skipping ahead doesn't count: marking complete unlocks after ~7 more min of reading.

Go deeper · reading trail

all reading →

Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.

GitHubPostmortem · ~14 min

October 21 post-incident analysis

Jason Warner · 2018
What
GitHub's postmortem of 24 hours and 11 minutes of degraded service in October 2018.
Key idea
A 43-second network partition made automated failover promote MySQL primaries in another region; writes landed on both sides and had to be reconciled by hand.
Why here
The failover chapter's warning made real: automatic promotion across regions plus replication lag can cost far more than the outage it reacts to.
taught here: Failover
UberBlog post · ~24 min

Why Uber Engineering Switched from Postgres to MySQL

Evan Klitzke · 2016
What
Uber's explanation of the storage and replication limits it hit with Postgres.
Key idea
Postgres replicated physical WAL changes, so write amplification on indexes multiplied replication traffic; MySQL's logical replication suited their workload better.
Why here
Makes the replication internals chapter concrete: what is shipped to replicas (physical pages vs logical rows) changes lag, bandwidth and upgrades.
taught here: Replication internals

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…