Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 7.4 of 7.8 · Advanced · 30 min read

Replication, partitioning and sharding

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Sooner or later, one database server runs out of something: availability, capacity or storage. In this lesson you'll learn the two ways to spread data over several machines, and what each does to correctness.

Imagine a popular library. To keep serving readers if one building floods, it keeps identical copies of its catalogue in several branches. That's replication. The copies are updated by courier, so a branch may be a little behind. When the collection is too big for one building, the library splits it, with half the alphabet here and half there. That's partitioning, often called sharding. Splitting well means no branch is overwhelmed, and readers rarely need to visit every branch for one answer. Real systems usually do both: each partition is replicated.

The most common scheme is leader and follower replication. All writes go to one node, the leader, which streams its change log to the followers. The key choice is when the leader reports a commit. With a synchronous follower, the commit waits until the follower has the change. A committed write survives losing the leader, but every commit pays a network round trip. With an asynchronous follower, the commit doesn't wait. It's fast, but if the leader dies, the last changes can be lost, even though the client was told they were committed.

Asynchronous followers are usually milliseconds behind, but under load the lag can grow to seconds or minutes. Users notice. A customer saves a new delivery address, and the leader says OK. She reloads the page. The read is served by a follower, and she sees her old address. It looks as if the save failed. Her change only reaches the follower after the lag. This broken guarantee is called read your writes. The fix is to read the user's own recent changes from the leader, or wait for the follower to catch up. Decide per query which reads may be stale.

When a leader fails, failover replaces it with a follower: detect the failure, choose the most up-to-date follower, and redirect writes. Each step can go wrong. The most dangerous failure is split brain: two nodes both believe they're the leader, both accept writes, and the data diverges. Protection needs a quorum, so a node may lead only while a majority agrees, and fencing, so the old leader cannot keep writing. GitHub's incident in October 2018 shows how hard this is. A forty three second network fault triggered failover to the other coast. Both coasts then held writes the other lacked, and service was degraded for over a day.

Now partitioning. Each record goes to one partition by its partition key, and the aim is to spread load evenly. Range partitioning gives each partition a range of keys, such as months. A scan of one month touches one partition. But if the key grows with time, every new order lands on the newest partition: a hot spot. Hash partitioning spreads new orders evenly across partitions. The price is that a scan of one month must ask every partition. Many stores combine the two. Hash the customer's identifier to pick the partition, and sort by time inside it, so one customer's history is a single scan.

Let's choose a shard key. The food delivery platform takes ten million orders a day, about three thousand five hundred writes a second at peak. One well-tuned primary can handle that, so throughput alone doesn't force sharding. Storage does. At about forty gigabytes a day, two years online is twenty nine terabytes, and rebuilding a replica on one node would take too long. About two terabytes a shard gives sixteen shards, each with two followers, with one thousand and twenty four logical partitions on top, so growth moves whole partitions instead of re-hashing keys. Then the candidate keys. The order identifier would scatter each customer's history. Time placed would be a hot spot. Restaurant has hot keys, and some cities are far bigger than others. So the decision is to hash the customer's identifier and sort by time placed. Other queries get separate paths, such as a read model for each restaurant's queue. Notice: with only ninety days online, no sharding would have been needed.

Four common mistakes. First, treating replicas as backups. A replica faithfully copies a mistaken delete within milliseconds. Second, trusting automatic failover that has never been rehearsed. Third, sharding too early, which makes joins, transactions and schema changes harder. Try indexing, caching, read replicas and archiving first. And fourth, sharding by a key that always increases, which sends every new write to one partition. A shard key is very hard to change later, so record the volumes it assumed.

The full section also covers leaderless replication, consistent hashing and secondary indexes. Next, in Section seven point five, we look at CQRS and event sourcing. See you there.

What you need to know

Two different reasons to spread data

A single database server eventually runs out of something: availability (it is one machine that can fail), read capacity, write capacity, storage, or proximity to users on other continents. There are two complementary ways to spread data over several machines, and it is important not to confuse them. Replication keeps a copy of the same data on several nodes, for availability, read scaling and lower latency near users. Partitioning, called sharding in many products, splits the data so that each node holds a subset, for write throughput and storage beyond one machine. Real systems usually do both: each partition is replicated.

Both have serious consequences for correctness. Replication raises the question of what a reader sees while copies disagree, which is the subject of consistency models, CAP and PACELC. Partitioning raises the question of what happens to queries, indexes and transactions that span partitions. This section explains the mechanisms in enough depth to judge a database's claims and to design a shard key.

Leader-follower replication

The most common scheme is leader-follower replication (also called primary-replica; older documentation says master-slave). One node, the leader, accepts all writes. It records every change in its log (PostgreSQL's write-ahead log, MySQL's binary log) and streams that log to followers, which apply the changes in the same order. Reads can go to the leader or to followers. PostgreSQL streaming replication, MySQL replication, SQL Server availability groups, MongoDB replica sets and most managed cloud databases work this way.

The key design choice is when the leader reports a commit as successful. With synchronous replication it waits until a follower confirms it has the change, so a committed write survives the loss of the leader; the price is that every commit includes a network round trip, and if the synchronous follower is down, writes stall. With asynchronous replication the leader confirms immediately and ships the change afterwards; commits are fast and followers can be far away, but if the leader dies, the last changes that had not reached any follower are lost even though the client was told they were committed. A common compromise, called semi-synchronous in MySQL, waits for one follower and lets the rest be asynchronous. In the bank, losing an acknowledged payment is unacceptable, so at least one synchronous copy (or a consensus-based database) is normal; for the food-delivery menu database, a few seconds of potential loss may be an acceptable price for speed. This is the RPO trade-off from availability and reliability in concrete form.

Leader with a synchronous and an asynchronous follower

Clients send writes to the leader, which streams its change log to one synchronous and one asynchronous follower; reads can go to followersApp serverswritesLeaderaccepts all writessync: commit waitsasync: commit doesnot waitFollower 1synchronousalways currentFollower 2asynchronousmay lagreadsreads (possibly stale)streams its change log(WAL or binlog)
All writes go to the leader; its change log flows to followers, and the commit waits only for the synchronous one.

Replication lag and the anomalies it causes

Asynchronous followers are usually milliseconds behind, but under load, during a large batch update or across a slow network, the replication lag can grow to seconds or minutes. Reading from followers then produces anomalies that users notice. Douglas Terry and colleagues defined the first two guarantees below, read-your-writes and monotonic reads, as session guarantees in 1994; the third matters most when data is also partitioned.

Read-your-writes violated

A user updates her delivery address on the leader, then immediately reads from a lagging follower and sees the old addressUserLeaderFollower1 save new address2 OK3 reload page4 old address!5 change arrivesafter the lagFix: read the user's own recent changes from the leader, or wait for the follower to catch up
The user's own change is acknowledged by the leader but not yet applied on the follower that serves her next read.

Lag anomalies and their fixes

GuaranteeWhat goes wrong without itCommon fixes
Read-your-writes (read-after-write)A customer changes her delivery address, reloads, and sees the old one: it looks as if the save failed, so she tries again.Read data the user may have just changed from the leader (for example, for one minute after their last write); or record the leader's log position at write time and read from a follower only once it has applied that position.
Monotonic readsA courier's app refreshes twice; the first read hits an up-to-date follower and shows an order as assigned, the second hits a lagging one and the order seems unassigned again. Time appears to go backwards.Route each user's reads to the same follower (for example, by hashing the user id), failing over only when it is unavailable.
Consistent prefix readsReaders see an answer before the question, or an order's delivered event before picked up, because the two were written to different partitions that lag differently.Write causally related data to the same partition, or track causal dependencies explicitly.

None of these anomalies exists if every read goes to the leader, so the simplest fix is often to send reads that must be fresh to the leader and use followers only for reads that tolerate staleness: product listings, reports, search feeds. Design this deliberately per query instead of routing all reads to replicas by default.

Failover and split brain

Failover replaces a failed leader with a follower. It has three steps: detect that the leader has failed (usually by a timeout), choose the new leader (ideally the most up-to-date follower, often by a consensus protocol such as Raft among the nodes or an external coordinator such as etcd or ZooKeeper), and redirect writes to it. Each step can go wrong. A timeout that is too short fails over during a brief network blip or load spike, which makes things worse; one that is too long extends the outage. With asynchronous replication, writes the old leader had not yet shipped are lost, and if the old leader comes back and still holds them, they conflict with new writes.

The most dangerous failure is split brain: two nodes both believe they are the leader and both accept writes, so the data diverges in ways that may be impossible to merge automatically. It typically follows a network partition in which the old leader is cut off but alive. Protection needs a quorum (a node may lead only while a majority agrees), fencing (the new leader's authority is enforced by an ever-increasing token that storage checks, or the old leader is forcibly powered off, a technique nicknamed STONITH: shoot the other node in the head), and clients that follow the current leader reliably.

GitHub's incident of 21 October 2018 shows how hard this is in practice. A network fault cut the connection between its US East Coast hub and its main East Coast data centre for 43 seconds. Its failover tooling, working as designed, promoted West Coast database primaries. When connectivity returned, both coasts held writes the other lacked, so GitHub could not simply fail back; to protect data integrity it chose to restore and reconcile, and service was degraded for 24 hours and 11 minutes. Automatic failover across regions had turned a 43-second blip into a day-long recovery.

Multi-leader and leaderless replication

In multi-leader replication several nodes accept writes and replicate to each other. It is used for multiple data centres (each region writes locally, so users avoid cross-ocean latency), for clients that work offline (a calendar or notes app is effectively a leader on each device) and for collaborative editing. The unavoidable problem is write conflicts: two leaders change the same record concurrently. Resolutions include last write wins (simple, but silently discards data and depends on clocks), merging values, keeping both and asking a person, or using data types designed to merge automatically (CRDTs, conflict-free replicated data types). The most reliable approach is to avoid conflicts: give each record a home region that alone accepts its writes, such as a customer's home country.

Leaderless replication, popularised by Amazon's Dynamo paper (DeCandia and colleagues, 2007) and used in Apache Cassandra, ScyllaDB and Riak, has no leader at all. The client, or a coordinator node acting for it, sends each write to all N replicas of a key and treats it as successful when W of them acknowledge; it sends each read to replicas and waits for R answers, taking the newest version. If R + W > N, every read set overlaps every successful write set in at least one replica, so a read normally sees the latest acknowledged write. Replicas that missed writes are repaired by read repair (the coordinator writes back the newest version when it sees a stale one) and background anti-entropy that compares replicas. Cassandra exposes these choices per request as consistency levels such as ONE, QUORUM and ALL.

A quorum write and read with N = 3

In a leaderless store with three replicas, a write goes to all three and succeeds when two acknowledge; a read asks all three, waits for two and takes the newest version, so read and write sets overlapWrite: N = 3, W = 2Read: R = 2ClientReplica Av2Replica Bv2Replica Cv1 (was down)2 acks: write succeedsClientReplica Av2Replica BslowReplica Cv1replies v2 and v1:newest (v2) wins;C repaired to v2R + W = 4 > N = 3: every readoverlaps the latest write
With W = 2 and R = 2, at least one replica in every read has seen the latest acknowledged write.

Quorums are often over-trusted. R + W > N does not by itself make the system linearisable: concurrent writes, writes that failed on some replicas but succeeded on others, sloppy quorums (writing to substitute nodes during failures) and last-write-wins with skewed clocks all allow stale or lost data. Chapter 6 works through quorum arithmetic and these edge cases in CAP and PACELC. Leaderless stores are excellent for high write availability across failures and data centres; they are a poor fit for invariants such as never sell the same seat twice unless they offer a lightweight transaction or consensus feature for those operations.

Partitioning: by key range or by hash

Partitioning assigns each record to one partition by its partition key (or shard key), and the aim is to spread data and load evenly. A partition that receives disproportionate load is a hot spot, and an uneven spread is called skew. There are two basic schemes.

Key-range partitioning gives each partition a contiguous range of keys, like volumes of an encyclopaedia. Range scans are efficient because adjacent keys are stored together (all orders for October in one or two partitions), and partitions can be split when they grow, as in Bigtable, HBase, Spanner and MongoDB's range sharding. The danger is a key that increases with time: if the key is a timestamp or a sequential order number, every new write lands on the last partition, which becomes a hot spot while the others sit idle. Hash partitioning applies a hash function to the key and assigns ranges of hash values to partitions, spreading even sequential keys uniformly. The price is that range queries on the key now have to ask every partition. Many stores combine the two with a compound key: Cassandra hashes the partition key to choose the node and sorts rows within the partition by clustering columns, so (customer_id, placed_at) puts one customer's orders together, in time order, on one node.

Range versus hash partitioning for orders

Range partitioning puts adjacent keys together so time-ordered keys all land on the newest partition; hash partitioning spreads keys evenly but scatters rangesBy key rangeBy hash of keyP1Jan to AprP2May to AugP3Sep to nowrange scan of one month:one partition (good)all new orders hit P3:hot spot (bad)P1hash 0 to 33P2hash 34 to 66P3hash 67 to 99new orders spread evenlyover P1, P2, P3 (good)range scan of one month:ask every partition (bad)Compound key: hash(customer_id) picks the partition,placed_at sorts rows inside it: one customer's history is one scan
Range partitioning keeps ranges together but concentrates time-ordered writes; hashing spreads writes but scatters ranges.

Consistent hashing and rebalancing

The obvious way to map hashes to nodes, hash(key) mod N, has a fatal flaw: when N changes, almost every key moves. Going from 4 to 5 nodes relocates about 80% of the keys (any key whose remainder differs for 4 and 5), and from 10 to 11 about 91%, which means copying almost the whole data set just to add one machine. Consistent hashing, introduced by David Karger and colleagues in 1997 for web caches, places both nodes and keys on a circular hash space; each key belongs to the next node clockwise. Adding a node takes over only the keys between it and its predecessor, about 1/N of the total, and removing one hands its keys to its neighbour. With few nodes the arcs are uneven, so each physical node is given many virtual nodes (tokens) at different points on the ring, which evens out the load and lets a new node take a little from every existing node. Dynamo and Cassandra use this scheme.

Consistent hashing on a ring

Nodes and keys are hashed onto a ring; each key belongs to the next node clockwise, so adding node D takes over only the keys between C's predecessor and DNode ANode BNode CNew node Dhash ringkeys go clockwiseAdding node Dhash mod N, 4 to 5 nodes:about 80% of keys moveconsistent hashing:only keys between B and Dmove (from C to D),about 1/N of the totalvirtual nodes: each servertakes many points on the ringto even out the load
Keys belong to the next node clockwise, so a new node takes over one arc instead of reshuffling everything.

Two other rebalancing strategies are common. With a fixed number of partitions, you create many more partitions than nodes at the start (say 1,024 for 10 nodes) and assign whole partitions to nodes; adding a node moves a few whole partitions and never re-hashes keys. Elasticsearch shards, Couchbase vBuckets and many hand-built sharding schemes use this, and the main decision is choosing a partition count large enough for future growth. With dynamic partitioning, as in HBase and MongoDB, a partition splits when it exceeds a size threshold and the halves can move to different nodes. Whatever the scheme, rebalancing moves large amounts of data over the network while the system is serving traffic, so many teams prefer that a human approves each move, and they throttle it to protect latency.

Requests also need routing to the right partition: either any node accepts a request and forwards it (Cassandra nodes learn the ring by gossip), or a routing tier knows the partition map (MongoDB's mongos), or the client library holds the map. The map itself is usually kept in a coordination service such as etcd or ZooKeeper or in the database's own consensus group.

Hot keys and skew

Hashing spreads keys evenly, not load. If one key is far more active than the rest, its partition is hot whatever the scheme: a celebrity's account in a social network, a national chain's flagship restaurant during a promotion, or a bank's own settlement account that touches every payment. Managed stores make the limits explicit; Amazon DynamoDB, for example, documents a ceiling per partition of 3,000 read units and 1,000 write units a second. Mitigations include caching hot reads, splitting a hot key by appending a small random suffix (writes to restaurant:88#0 to restaurant:88#9, at the cost of reading all ten and combining), moving a very large tenant to dedicated partitions, and aggregating counters in memory before writing them. All of them add complexity, so first measure which keys are actually hot.

Secondary indexes across partitions

Partitioning works smoothly for lookups by the partition key. Queries by anything else need a secondary index, and there are two ways to partition one. A local index (document-partitioned) lives with each partition and covers only that partition's rows. Writes update a single partition, but a query by the indexed attribute must be sent to every partition and the results merged (scatter-gather), so its latency is set by the slowest partition and its cost grows with the number of partitions. A global index (term-partitioned) is itself partitioned by the indexed value, so a query reads one index partition; but a single write may now need to update index partitions on other nodes, which is usually done asynchronously, so the index can briefly lag the data. DynamoDB shows both: a local secondary index shares the table's partition key, while a global secondary index has its own and is eventually consistent.

Local and global secondary indexes

A local index lives with each partition, so writes are local but a query by restaurant asks every partition; a global index is partitioned by the indexed value, so the query asks one index partition but each write may update an index on another nodeLocal (per-partition) indexGlobal (term-partitioned) indexPartition 1orders by customerindex: restaurant(own rows only)Partition 2orders by customerindex: restaurant(own rows only)Queryrestaurant = 88scatter to all, gather resultsPartition 1orders by customerPartition 2orders by customerIndex part arestaurants 1-50Index part brestaurants 51-99asyncQueryrestaurant = 88one index partition;may be slightly stale
A local index makes writes cheap and queries scatter; a global index makes queries targeted and index updates remote and usually asynchronous.

Worked example

Worked example: sizing and choosing a shard key for orders

The food-delivery platform has grown to 30 countries and 10 million orders a day, with the evening peak at three times the average rate. Each order generates about 10 writes over its life (creation, lines, status changes, payment references) and occupies about 4 KB including indexes. Customers can view their full order history; regulation requires orders to be retrievable for six years, but support staff need online access only to the last two.

Write rate10,000,000 / 86,400 s = about 116 orders a second on average; peak about 350 a second; at 10 writes each, about 3,500 writes a second at peak. A well-tuned single primary can handle this; throughput alone does not force sharding.
Storage growth10,000,000 x 4 KB = 40 GB a day, about 14.6 TB a year, about 29 TB for two years online.
Do we need to shard?If only 90 days had to be online (3.6 TB), one primary plus replicas, with time-based table partitioning and archiving to the warehouse, would be the boring and correct answer. With two years online at 29 TB, restore and replica rebuild times on one node become unacceptable, so the team shards.
Size the shardsTarget about 2 TB per shard so a replica can be rebuilt in hours: 29 TB / 2 TB = about 15, so 16 shards, each a leader with two followers. Create 1,024 logical partitions mapped onto the 16 shards so growth means moving whole partitions, not re-hashing. Peak writes per shard: 3,500 / 16 = about 220 a second.
Candidate: order_id (hash)Perfectly even writes, but a customer's history and a restaurant's live queue both become scatter-gather across 16 shards. Rejected as the primary key for placement.
Candidate: placed_at (range)Great for time-range reports, but every new order writes to the newest partition: a guaranteed hot spot. Rejected.
Candidate: restaurant_idA restaurant's queue is local, but large chains and promotions create hot keys, and customer history scatters. Rejected.
Candidate: country or citySupports data residency and local queries, but one large city may carry 15% of all orders: skew. Kept only as a coarse first level for residency (EU and non-EU clusters), not as the shard key.
DecisionShard by hash(customer_id) with rows sorted by placed_at inside each partition. The order id embeds the customer's partition number so a lookup by order id goes straight to one shard.
Covering the other queriesThe restaurant's live queue is served by a separate read model keyed by restaurant and built from order events (a deliberate, eventually consistent global index). Analytics run in the warehouse. Cross-customer transactions are not needed: each order touches one customer.

The numbers changed the question. Throughput never required sharding; data retained online did. Shrinking the online window would have removed the need entirely, which is worth raising with the business before building a sharded system.

The shard key was chosen from the most important queries and their consistency needs, not from the data model alone. Every other query was given an explicit, separately designed path.

Choosing a shard key is one of the hardest decisions to reverse: changing it later means moving every row. Record it in an ADR with the volumes and queries it assumed.

Common mistakes

Treating replicas as backups. A replica faithfully copies a mistaken DELETE within milliseconds; backups and point-in-time recovery are still needed (Chapter 0). Reading from replicas by default without deciding which queries tolerate lag. Trusting automatic failover that has never been rehearsed, or that can promote a node while the old leader still accepts writes. Sharding too early: a sharded database makes joins, transactions, schema changes and operations harder, and modern single nodes are very large; exhaust indexing, caching, read replicas, archiving and vertical scaling first. Sharding by a monotonically increasing key, which concentrates every insert on one partition. Forgetting secondary access paths, discovering after the split that an important query must now ask every shard. As the rest of this chapter shows, the good alternatives often involve derived data: read models, caches and analytical stores, each built for one kind of query.

Check your understanding

Why does asynchronous replication risk losing writes that clients were told had committed?

The leader acknowledges before any follower has the change; if it fails before shipping it, the promoted follower never received those writes.

A user saves a change and immediately sees the old value. Which guarantee is broken, and what is one fix?

Read-your-writes. Serve that user's reads of recently changed data from the leader, or wait until the follower has applied the write's log position.

Why is hash mod N a poor way to assign keys to nodes?

Changing N moves most keys (about 80% when going from 4 to 5 nodes), whereas consistent hashing or fixed logical partitions move only about 1/N.

What is the trade-off between local and global secondary indexes?

Local indexes keep writes on one partition but force queries to ask every partition; global indexes make queries target one index partition but require cross-partition, usually asynchronous, index updates.

Next · 7.5CQRS and event sourcingSeparating the model that changes data from the models that read it, storing state as an append-only sequence of events, and the real costs: eventual consistency, event versioning and personal data.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our orders table is approaching 5 TB on one PostgreSQL server. Should we shard it, and if so, how do we choose the shard key?”