Database Replication and Sharding
How one database becomes many. Replication for read scale and availability, sharding for write scale, and the operational traps in both.
Covers: Leader-follower replication, replication lag, read-your-writes, partitioning strategies, hot shards, resharding
There are exactly two ways to make a database handle more: copy it (replication) or split it (sharding). Replication scales reads and buys availability. Sharding scales writes and storage. They solve different problems, they are usually used together, and confusing them is a reliable way to lose an interview.
30-second answer
The write went to the primary and the subsequent read was served by a replica that had not yet applied it - replication lag. The fix is read-your-writes consistency, and there are four standard approaches: route reads to the primary for a short window after a user writes, pin that user's session to the primary, have the client pass the log position it wrote at and wait for a replica to catch up, or make the UI optimistic and render the value it just submitted. Route-to-primary-after-write is the usual pragmatic choice.
Where the stale read comes from
The write and the read take different paths. Nothing is broken - this is asynchronous replication behaving exactly as designed.
Flow
- UserApp server
- App serverPrimary(write)
- PrimaryReplica A(replicate)
- PrimaryReplica B(replicate)
- App serverReplica B(read (stale!))
The four fixes, in order of how often they are the right answer
| Approach | How it works | Cost |
|---|---|---|
| Read from primary after write | For N seconds after a user's write, route that user's reads to the primary | Extra primary load; needs a per-user timestamp, usually in a cookie or Redis |
| Monotonic / sticky routing | Pin a session to one replica so it never goes backwards in time | Fixes monotonic reads, not read-your-writes; uneven replica load |
| Log-position token | Client receives the LSN/GTID it wrote at and passes it on reads; replica waits or is chosen | Most precise, but requires client and driver support |
| Optimistic UI | Render the submitted value locally without re-reading | Free and effective for the common case; diverges if the write actually failed |
WRITE_WINDOW = 10 # seconds - comfortably above observed p99 replication lag
def route_read(user_id):
last_write = redis.get(f"lw:{user_id}")
if last_write and time.time() - float(last_write) < WRITE_WINDOW:
return primary # this user only, for a short time
return pick_replica()
def on_write(user_id):
redis.setex(f"lw:{user_id}", WRITE_WINDOW, time.time())
# Choose WRITE_WINDOW from measured p99 lag, and alert if lag ever exceeds it -
# otherwise the guarantee silently stops holding.Synchronous, semi-synchronous, asynchronous
| Mode | Primary waits for | Data loss on primary failure | Write latency |
|---|---|---|---|
| Asynchronous | Nothing | Possible - unreplicated commits are lost | Lowest |
| Semi-synchronous | At least one replica to acknowledge | Very unlikely | +1 network round trip |
| Synchronous (all) | Every replica | None | Slowest, and one slow replica stalls all writes |
Semi-synchronous is the usual production compromise: durable enough to survive losing the primary, without letting the slowest replica dictate write latency.
Say these points
- Stale reads after a write are replication lag, not a bug.
- Read-your-writes via a short primary-read window is the common pragmatic fix.
- Distinguish read-your-writes, monotonic reads and consistent prefix by name.
- Alert on replication lag and fail back to the primary when it exceeds your window.
- Semi-synchronous replication is the usual durability/latency compromise.
Avoid these mistakes
- Sending all reads to replicas without considering the write path.
- Assuming lag is always milliseconds.
- Using sticky replica routing and calling it read-your-writes.
- Failing over to a replica that is behind, and losing acknowledged writes.
Expect these follow-ups
- The primary dies with 3 seconds of unreplicated writes. What do you do?
- How do you run a schema migration without spiking replication lag?
- How does multi-leader replication change this picture?
30-second answer
Split data across independent databases by a shard key. Range partitioning keeps ranges together - great for range scans, terrible for hotspots, since recent data all lands on one shard. Hash partitioning distributes evenly but destroys range queries. Directory partitioning uses an explicit lookup table, giving full flexibility at the cost of an extra hop and a component that must not become a bottleneck. Choose a key that is present on almost every query, has high cardinality, and distributes evenly - and accept that queries not containing the key must scatter-gather across every shard.
Showing 2 of 2 questions for database-replication-and-sharding.
Check your understanding
4 questions · no sign-up, nothing stored
Hands-on challenge
Build it - this is what you talk about in a deep-dive round.
Shard a messaging system's storage
A chat product stores 50 billion messages, ingests 100,000 writes/second, and must load any conversation's last 50 messages in under 50 ms.
Requirements
- Choose a shard key and defend it against all four tests: query coverage, cardinality, distribution, colocation.
- Specify the partitioning strategy and the logical-to-physical shard mapping.
- Show how 'last 50 messages in a conversation' is served by exactly one shard.
- Design globally unique, time-sortable message IDs.
- Handle a 500,000-member channel that dwarfs every other conversation.
Stretch goals
- Design full-text search across all shards without scatter-gathering on every query.
- Write the runbook for splitting the hottest shard in two with no downtime.