Scaling the app tier is easy — just add more servers. Scaling the database is the hard part, and replication is usually the first tool we reach for. In simple language — replication means keeping live copies of our database on other servers, so we can read from many machines instead of pounding one.
Primary handles writes, replicas serve reads
The classic setup is one primary, many replicas (also called leader/follower or master/slave).
- The primary is the single source of truth. Every
INSERT,UPDATE, andDELETEgoes here. - Each replica is a read-only copy. The primary streams its changes to them, and they apply those changes to stay in sync.
- Our app sends writes to the primary and spreads reads across the replicas.
Why this works so well: most apps read way more than they write — often 90%+ reads. Timelines, product pages, search results are all reads. By pushing all those reads onto replicas, we take a huge load off the primary, and we can add more replicas as read traffic grows.
reads 📖
reads 📖
reads 📖
Sync vs async replication
The big question: when the primary confirms a write, has it actually reached the replicas yet?
- Asynchronous — the primary commits and replies “done” immediately, then streams the change to replicas afterward. Fast writes, but replicas trail slightly behind. If the primary dies right after committing, a few just-written rows might not have made it out yet.
- Synchronous — the primary waits until at least one replica confirms it got the change before replying “done”. No data loss on failover, but every write is slower because it waits for a network round-trip.
Most systems run async by default (speed matters, and the tiny window is acceptable), and use sync only where losing a write is unacceptable — think payments. Think of it like — async is mailing a copy and moving on; sync is waiting for the recipient to sign for it before we do anything else.
Replication lag & read-your-writes
Because async replicas trail the primary, there’s a gap called replication lag — usually milliseconds, sometimes seconds under load. This causes a genuinely nasty bug: read-your-writes.
Picture this: a user updates their profile (write → primary), the page reloads and reads from a replica — but the replica hasn’t caught up yet, so the user sees their old profile and thinks the save failed.
// user just saved — this write hit the PRIMARY
await db.primary.query("UPDATE users SET bio = $1 WHERE id = $2", [bio, id]);
// immediate reload reads a REPLICA that may still be behind → stale bio 😱
const user = await db.replica.query("SELECT bio FROM users WHERE id = $1", [id]);
Common fixes: read from the primary for a short window right after a user’s own write, pin that user to the primary temporarily, or wait until the replica has caught up to the write’s position. The key interview point is just naming the problem — “async replication means reads can be stale, so read-your-writes needs care.”
Failover & promotion
Only the primary takes writes, so if it dies, writes stop entirely until we fix it. Failover is the process of promoting one of the replicas to become the new primary.
- Manual failover — a human (or runbook) picks a replica and promotes it. Safe, slow.
- Automatic failover — a tool (Patroni, RDS Multi-AZ, etc.) detects the dead primary, promotes the most up-to-date replica, and repoints the app. Fast, but needs care to avoid split-brain — two nodes both thinking they’re the primary and accepting conflicting writes.
After promotion, the app must be told the new primary’s address (usually via a floating DNS name or a proxy), and the old primary, once healed, rejoins as a replica.
Practical takeaway
Replication gives us read scaling and high availability, not write scaling — the single primary is still the write bottleneck (that’s what sharding is for, next note). Default to async replication, route reads to replicas but be deliberate about read-your-writes, and have a tested failover plan so a dead primary doesn’t mean a dead app.