A single database server, like a single application server, eventually runs out of capacity — but unlike a stateless application server, you can't just clone a database and round-robin between identical copies, because a copy that isn't kept in sync with the original stops being a correct copy at all. Database scaling is really about deciding which correctness guarantees you're willing to relax to get more capacity.
Leader-follower replication (also called primary-replica or master-slave) designates one database instance as the leader, which accepts all writes, and one or more followers, which continuously replicate the leader's changes and serve reads:
Client writes ----> Leader ----(replication stream)----> Follower 1
| -> Follower 2
| -> Follower 3
Client reads ----> any Follower (or the Leader)
This solves the most common real-world database bottleneck directly: most applications are read-heavy, often overwhelmingly so, and read replicas let you scale read capacity horizontally — add more followers, spread the read traffic across them — while write throughput is still ultimately bounded by whatever a single leader can handle. If the leader fails, one follower is typically promoted to take its place (failover), though that promotion takes time and, depending on how replication was configured, can lose the most recent writes that hadn't finished replicating yet.
The trade-off that follows directly from this design: replication to a follower is rarely instantaneous, so a follower can be serving stale data for a brief window right after a write — read your own write from a follower that hasn't caught up yet, and you might not see it. This is the first concrete instance of the strong-vs-eventual-consistency trade-off covered fully in the next chapter.
Read replicas scale reads, but every one of them still holds a full copy of all the data, and write throughput is still capped by one leader. Sharding (horizontal partitioning) solves the deeper problem by splitting the data itself across multiple independent database instances, each holding only a slice of the total rows — which scales both storage and write throughput, because different shards can accept writes for their own slice completely independently.
| Strategy | How it splits data | Strength | Weakness |
|---|---|---|---|
| Range-based | Rows are split by a value range on the shard key (e.g., user IDs 1–1M on shard A, 1M–2M on shard B) | Range queries ("all orders from January") stay efficient — they hit one or a few contiguous shards | Uneven distribution if data or traffic isn't uniform across the ranges — one range can become a hot shard |
| Hash-based | The shard key is hashed, and the hash determines which shard a row lives on | Spreads data (and load) evenly across shards, regardless of any pattern in the key values | A range query now has to fan out to every shard, since consecutive keys are scattered across all of them |
Which one is right depends entirely on the access pattern: an application that constantly queries by a date or ID range benefits from range-based sharding despite the hot-shard risk, while an application whose access pattern is scattered and unpredictable is usually better served by hash-based sharding's more even distribution, at the cost of losing efficient range scans.
Sharding is usually the last scaling lever reached for, not the first, because it's genuinely expensive in ways that aren't obvious from a one-paragraph description:
Common mistake: Reaching for sharding as a first move because "the database needs to scale," when the actual bottleneck is read traffic that read replicas and caching would have solved with a fraction of the operational cost. Sharding is the right call once write throughput or total data volume genuinely can't fit on one leader — not a default starting point.