System designCore13 min
Sharding & partitioning
When one database can't hold all the data or take all the writes, you split the rows across many. Sharding buys almost unlimited room, and the shard key you pick decides which queries stay fast and which get slow for good.
1 · The problem
One database can only grow so far
An online store keeps every order in one database. It has 4 TB of orders and grows by a terabyte a year. At the holiday peak it must take 30,000 new orders a second, and load tests show the server tops out around 10,000. Something has to give.
Try the cheap fixes first, because each one keeps the database simple:
Fix | What it helps | Where it stops |
|---|---|---|
A bigger server | Everything, with no code changes | The biggest machine you can buy, and its price |
Read replicas | Reads: copies of the data answer queries | Every write still goes to one leader |
A cache | Repeated reads of the same data | Writes, and data read only once |
Archiving old rows | Size: orders from 2019 move to cheap storage | Write rate |
None of these raises the write limit. For that the data itself has to be split, so that different servers own different rows. Partitioning means splitting a dataset into pieces. Sharding is partitioning by rows across separate servers: each shard has the same tables but its own subset of the rows, and each shard is a complete database of its own.
With four shards, each takes a quarter of the peak: 30,000 ÷ 4 = 7,500 writes a second, below the 10,000 one server can handle. Each shard is still replicated, so losing a server affects one shard until a replica takes over, never the whole store:
Sharding and replication solve different problems and are used together: shards split the writes, replicas keep each shard alive and serve its reads.
Sharding is one of three ways to split data, and the other two are simpler, so check them first:
- Vertical partitioning splits a table's columns: the order's large, rarely read gift message moves to its own table.
- Functional partitioning gives each feature its own database: orders, the product catalog and reviews no longer share a server.
- Horizontal partitioning, or sharding, splits the rows. It is the only one that keeps scaling, and the only one that changes how every query works.
The names vary by product. MongoDB and Elasticsearch say shard; DynamoDB and Kafka say partition; HBase says region. The idea is the same. One more word: databases can also partition a table inside one server, which helps manage big tables but adds no capacity.
2 · The big decision
The shard key decides which queries stay fast
Every row needs a rule that says which shard owns it. The column the rule uses is the shard key (or partition key). A query that names the shard key goes to one shard. A query that doesn't has to ask all of them. So the right key depends on the questions you ask most.
The store asks three questions: show a customer's orders (on every visit to the account page, by far the most common), open order 7731 (from an email link), and what sold today (once a night, for a report). Here is how three candidate keys fare:
Shard key | Writes | "My orders" | Verdict |
|---|---|---|---|
Order date, in ranges | Every new order lands on the shard holding today: one hot shard | Every shard, since a customer's orders span years | Fails at the peak |
Hash of the order ID | Spread evenly | Every shard: orders are scattered | Even, but the main query got slow |
Hash of the customer ID | Spread evenly across millions of customers | One shard: all of a customer's orders live together | The fit, if order IDs carry the customer |
The last row needs one trick. Open order 7731 names only the order, so the router can't tell which shard to ask. Fix it by building the customer's shard into the order ID itself (for example c51-7731), or by keeping a small lookup table from order ID to customer. The nightly report reads every shard either way; it runs once a night, so that is fine.
A good shard key has five properties:
- Many distinct values. Shard by country and you can never have more shards than countries; shard by continent and a cluster has at most seven useful shards.
- Evenly used values. If most rows share a few values, those shards carry most of the load (Hot partitions & skew covers the famous customer or celebrity).
- Not always increasing. Timestamps and auto-increment IDs send every insert to the shard holding the newest values.
- Never changes. A row whose key changes has to move to another shard.
- In your main queries. So most requests touch exactly one shard.
Take the time to choose. Changing the shard key later means moving every row to a new layout while the system is live, which is one of the riskiest jobs in data engineering.
3 · Placing rows
Ranges, hashes or a directory: three ways to place a row
Once you have the key, you need the rule that turns a key value into a shard. There are three common rules, plus one for geography:
Rule | How it places a row | Good at | Watch out for |
|---|---|---|---|
Range | Each shard owns a span of keys: A–F, G–M, and so on | Range queries: neighbors sit together, in order | Hot spots when new keys cluster at one end |
Hash | A hash of the key picks the shard | Spreading load evenly with no map to maintain | Range queries ask every shard; growing needs care |
Directory (lookup) | A lookup table maps each key, or group of keys, to a shard | Full control: move one big customer on their own | The directory must be fast, cached and never down |
Geographic | The user's region picks the shard | Data residency laws, and low latency for local users | One busy region overloads its shards |
Real systems often combine two. A compound key has a partition part and a sort part. Hash the customer ID to pick the shard, then keep that customer's orders sorted by date inside it. My orders, newest first becomes one shard and one sorted read. Cassandra's partition and clustering keys, and DynamoDB's partition and sort keys, work exactly like this.
Lookup tables become more flexible with virtual shards: the key maps to one of many small logical shards, say 1,024, and a small table maps those onto the physical servers. The application only ever sees the logical shard, so moving one to a new server is a change to the small table, not to the code.
4 · Reading it back
A query without the shard key asks every shard
Something has to know which shard owns what. Three places are common:
- A routing tier, such as MongoDB's
mongos: applications talk only to it, and it holds a cached copy of the shard map. - The client library, which keeps the map itself and connects straight to the right shard.
- Any node, as in Cassandra: the client asks any server, which forwards the request to the owner.
Whichever you pick, the map itself lives somewhere highly available, and every router caches it.
A query with the shard key is targeted: one shard answers. A query without it is a scatter-gather query. The router sends it to every shard, waits for all of them, and merges the results:
"Which orders have a refund pending?" doesn't name a customer, so all four shards answer, and the reply waits for the slowest of them.
Scatter-gather costs more than its share of work. Every shard spends effort on every such query, so adding shards doesn't add capacity for it. And the answer waits for the slowest shard. If each of 16 shards is slow 1% of the time, a query that asks all of them is slow 1 − 0.99¹⁶ ≈ 15% of the time.
Sorting and limits are merged at the router. For the 10 newest pending refunds, each shard returns its own 10 newest, and the router merges 40 rows and keeps 10. Paging deep into such results gets expensive, because to skip 1,000 rows the router has to fetch 1,010 from every shard.
Secondary indexes face the same choice. An index on status can live with the rows or be split by the indexed value:
Aspect | Local index | Global index |
|---|---|---|
Where it lives | Each shard indexes only its own rows | Its own partitions, split by the indexed value |
A write | Updates the row and the index on one shard, together | Also updates the index's partition, usually a moment later |
A query on | Asks every shard | Asks one index partition |
Fresh? | Always | Can lag briefly: DynamoDB's global indexes are eventually consistent |
5 · The catch
Anything that spans shards gets hard
Inside one database, a join or a transaction across tables is routine. Across shards, each becomes a distributed problem. Sharding works when you design the data so they are rare:
Need | Why it hurts across shards | What to do instead |
|---|---|---|
Joins | The rows live on different servers, so the database can't join them for you | Shard related tables by the same key: a customer, their orders and their addresses together |
Small shared tables | Every order needs tax rates and currencies | Copy reference data to every shard, and accept a short delay when it changes |
Transactions | An atomic change across shards needs a protocol like two-phase commit: slower, with new failure modes | Keep each transaction on one shard; use sagas when you can't (Distributed transactions: 2PC & sagas) |
Unique IDs | Each shard's auto-increment counter starts at 1, so IDs collide | Generate IDs that are unique everywhere (Unique ID generation) |
Uniqueness rules | "One account per email" must check every shard | A separate table keyed by email, or a check through a global index |
Operations multiply too. Every schema change runs on every shard. Backups are per shard, and restoring one shard to last Tuesday leaves it out of step with the others. Monitoring needs a view across all of them. None of it is impossible, and all of it is permanent: sharding is a decision you live with for the life of the system.
That is why it comes last. If the data and the writes fit on one well-sized server for the next few years, with replicas for reads and a cache in front, that is the better design. Shard when the write rate or the data size says you must, or when the law says some data must live in one country.
6 · Growing
Add shards without moving everything
The store grows, and four shards become five. How much data has to move? It depends on the rule, and the obvious rule is the worst one.
The obvious rule is hash mod N: shard = hash(key) mod 4. Change N to 5 and a key stays put only if its hash gives the same remainder for 4 and for 5, which is true for just 1 key in 5. So 80% of all orders move, at the busiest time, just to add 20% more capacity. Consistent hashing shows the problem in full.
Rows that move when 4 shards become 5. Mod N: a key stays only if hash mod 4 = hash mod 5, true for 1 key in 5. The other rules move about 1 ÷ 5 of the rows, the share the new server should end up holding anyway.
Many fixed partitions is a common answer. Create far more partitions than servers on day one, 1,024 for 4 servers, 256 each, and never change that number. The key-to-partition rule stays fixed forever; growing only moves whole partitions. The fifth server takes about 51 from each of the others. The catch is that the number can't easily change later: Elasticsearch, for example, sets an index's number of primary shards when the index is created.
Splitting ranges is the other answer, used by range-sharded systems like MongoDB and HBase. A range that grows too big splits in two, and a balancer moves halves to emptier servers. It adapts to the data, but it can't help when every new write targets the same end of the range.
Moving a partition safely follows the same steps whatever the rule:
- Copy
Copy the partition's rows to the new server while the old one keeps serving.
- Catch up
Replay the writes that arrived during the copy, until the new copy is only moments behind.
- Switch
Once the copy has caught up, update the shard map: reads move to the new server first, then writes.
- Refresh and clean up
Routers drop their cached map and learn the new owner; the old copy is deleted later, once nothing reads it.
Check yourself
5 questions
Takeaways
Remember this
- Shard when writes or data outgrow one server; replicas, caches and bigger machines only postpone that for reads and size.
- The shard key decides everything: many distinct values, evenly used, not always increasing, never changing, and present in your main queries.
- Queries with the shard key touch one shard; queries without it scatter to all and wait for the slowest.
- Keep data that is used together on the same shard, because joins, transactions and unique checks across shards are expensive.
- Never place rows with hash mod N; use many fixed partitions, splitting ranges or consistent hashing so growing moves only a fraction.