Sharding & partitioning
When one node can't hold the data or absorb the writes, split it. Range, hash, and directory partitioning each balance differently — and all of them can produce a hot shard, a painful resharding, and queries that no longer fit on one node.
A social app sharded its database by user ID — clean, even, every shard holding a few million users. Then a celebrity with 80 million followers posted, and one shard’s CPU pinned at 100% while the other nineteen sat near-idle. The “evenly balanced” cluster had a single hot shard carrying a wildly disproportionate load, and there was no quick fix: you can’t move part of a user to another node, and re-splitting the whole keyspace under live traffic is the scariest operation in the building. Sharding is how you scale past one machine — and the partition key you pick on day one decides, irreversibly, where your hot spots and your most painful migrations will live.
Replication is not enough
Replication copies the whole dataset to every node, so it scales reads and survives failures — but every node still holds everything, and every node still takes every write. Once the data won’t fit on one disk, or the write rate exceeds what one leader can absorb, copying doesn’t help; you have to split. Sharding (horizontal partitioning) divides the dataset into disjoint pieces — shards or partitions — and puts each on a different node, so each node holds only 1/N of the data and takes only its slice of the writes.
(A note on words: “partitioning” inside one database engine — Postgres declarative partitions, say — and “sharding” across separate database servers are the same idea at different scopes. The databases track’s sharding unit goes deep on the single-engine mechanics; here we reason about the distribution choice itself.)
The decision that governs everything is the partition key: the field whose value decides which shard a row lives on. Pick it well and load spreads evenly and your common queries stay on one shard. Pick it badly and you get the hook’s hot shard and cross-shard queries that fan out to every node.
Three ways to assign keys to shards
Which strategy you pick is a decision you’ll live with for years — so it’s worth understanding the exact trade-off each one buys before you write the first migration script.
range: [A–F] → shard 0 [G–M] → shard 1 [N–Z] → shard 2
hash: shard = hash(key) mod N (spreads neighbours apart)
directory: lookup table: key → shard (explicit, flexible, one more hop)- Range partitioning keeps keys in sorted order, so range scans (“all orders from March”) hit one shard and stay efficient. The danger is that ordered keys cluster: shard by timestamp and all of today’s writes land on the last shard — a moving hot spot.
- Hash partitioning runs the key through a hash and assigns by the result, scattering neighbouring keys across shards so load spreads evenly. The cost is that range scans now hit every shard, because adjacent keys are deliberately far apart.
- Directory partitioning keeps an explicit lookup table mapping keys (or key ranges) to shards. Maximum flexibility — you can move a hot key to its own shard — at the cost of an extra lookup and a directory that is itself critical infrastructure.
Together these three strategies form a dial: range gives you query efficiency at the cost of write hot-spots on ordered keys; hash gives you even write load at the cost of range scans; directory gives you flexibility at the cost of an extra hop and a new piece of critical infrastructure to maintain. Without understanding this dial, you’ll reach for hash by default and only discover the range-scan penalty in a production incident.
A multi-tenant SaaS stores order records sharded across 20 nodes. The dominant query is 'all orders placed in March' and inserts arrive on auto-incrementing order IDs. Which partitioning strategy fits best?
The hot-shard / celebrity problem
Even partitioning evenly by key count does not partition evenly by load. The hook’s celebrity is the canonical case: one key (a user, a product, a video) receives orders of magnitude more traffic than the average, so its shard saturates while the rest idle. This is skew, and it defeats the naive “just hash the key” answer, because all of one hot key’s traffic still lands on the one shard that owns it.
Mitigations all amount to spreading a hot key’s load:
- Split the hot key: append a random suffix (
celebrity_id:00…:09) so its writes scatter across ten logical keys on different shards, and fan reads back in. You trade simplicity for spread. - Cache the hot key in front of the shard so most reads never reach it (the next unit’s caching lessons).
- Isolate it: give the known hot key its own dedicated shard (directory partitioning makes this a one-row change).
Together these three mitigations reduce the traffic that reaches the hot shard — splitting moves writes to many shards, caching absorbs reads before they arrive, and isolation gives the hot key its own capacity budget. Without at least one of them, a single celebrity key can saturate a shard no matter how many nodes the rest of the cluster has.
The senior reflex: a uniform key distribution does not imply a uniform load distribution. You must reason about the access pattern, not just the key space.
▸Why this works
Why is choosing the partition key the highest-stakes decision and so hard to undo? Because the key is baked into where every row physically lives, and changing it means moving data. If you shard orders by order_id but your hottest query is “all orders for customer X,” every such query now fans out to all shards and gathers results — a scatter-gather that is slow and that scales its tail latency with shard count (lesson 01-scalability’s fan-out problem). Re-keying to customer_id later means re-reading and re-placing the entire dataset under live traffic. The discipline: choose the key from your dominant access pattern and your cardinality, not from what’s convenient — high cardinality to avoid hot spots, and aligned with the field you most often filter or join on so common queries stay single-shard.
Resharding pain
Sooner or later you outgrow your shard count and must reshard — add shards and redistribute data. With the naive hash(key) mod N scheme this is brutal: change N and almost every key maps to a different shard, so you must move nearly the whole dataset at once, while serving live reads and writes against keys that are mid-flight. This is precisely the “rehash everything” pain that consistent hashing (the next lesson) exists to solve — it lets you add a shard and move only ~1/N of the keys instead of all of them.
Even with consistent hashing, resharding is an operational marathon: you copy data while it’s changing, keep a record’s old and new home in sync until cutover, double-write or use change-data-capture to avoid losing writes during the move, and flip routing atomically. Teams that designed for resharding (more logical shards than physical nodes, so growth is “move a shard,” not “re-split the keyspace”) have a far easier time than teams who must split a live keyspace for the first time during a capacity crisis.
Cross-shard joins, transactions, and secondary indexes
Splitting data breaks the two things a single database gave you for free:
- Cross-shard joins no longer happen in the engine. A join between data on different shards must be done by the application (or a query layer): scatter-gather the pieces and join them in memory. It works, but it’s slow and fragile, which is why shard keys are chosen so that data joined together lives together (colocation — e.g. shard a customer’s orders onto the same shard as the customer).
- Cross-shard transactions lose single-node ACID atomicity. Updating two rows on two shards atomically needs a distributed transaction (two-phase commit) or a saga — a sequence of local transactions with compensating undo steps. Both are far more expensive and failure-prone than a single-node
BEGIN…COMMIT; the distributed track’s sagas unit covers the pattern in depth. - Secondary indexes stop being global. If you shard by
user_idbut need to query byemail, the email index is scattered across all shards. You either keep a local index per shard (then a query by email must hit every shard — scatter-gather) or a global index that is itself a separate sharded structure (kept consistent with the base data, often asynchronously, so it can lag). DynamoDB’s global secondary indexes, for instance, are eventually consistent for exactly this reason.
▸Common mistake
A common and expensive mistake is treating a sharded store like a single database and writing queries that quietly fan out to every shard. A dashboard that does SELECT … WHERE email = ? against a user_id-sharded cluster looks fine in dev (one shard) and falls over in prod (a scatter-gather across 200 shards, with tail latency set by the slowest shard — lesson 01-scalability again). The discipline: know your shard key, make your hot-path queries include it so they’re single-shard, and treat any query that doesn’t carry the shard key as a scatter-gather to be designed for deliberately (a global index, a cache, or an async read model) — never to be discovered in an incident.
You shard a time-series table by timestamp using range partitioning. Writes feel fine in testing but one shard's CPU pins in production while the others idle. Why?
Your cluster is sharded by user_id with hash(user_id) mod 8. You need to grow to 12 shards. What's the core problem, and what design avoids it next time?
A query that does not carry the shard key cannot be routed to one shard, so it must fan out to every shard and combine the results — a pattern called _______, whose tail latency is set by the slowest shard it touches.
- 01Compare range, hash, and directory partitioning by what each is good and bad at.
- 02What is the hot-shard problem and how do you mitigate it?
- 03Why does sharding break joins, transactions, and secondary indexes, and what replaces each?
When replication isn’t enough — the data won’t fit, or one leader can’t absorb the writes — you shard: split the dataset into disjoint partitions by a partition key, so each node holds only its slice. The scheme decides the trade-offs: range gives efficient scans but hot-spots on monotonically increasing keys, hash spreads load evenly but kills range scans, directory is flexible but adds a hop and a critical lookup table. The defining hazard is skew — a celebrity key whose load saturates one shard while the rest idle, because even key counts don’t mean even load; you split, cache, or isolate the hot key. Resharding under hash mod N moves nearly every key at once, which is why the next lesson’s consistent hashing (move only ~1/N) and provisioning many logical shards up front matter. And splitting breaks the freebies of one node: cross-shard joins become application-side scatter-gather (so you colocate), cross-shard transactions need 2PC or sagas, and secondary indexes become local-and-scattered or global-but-lagging. The single highest-stakes call is the partition key — choose it from your dominant access pattern and cardinality, because it irreversibly fixes where your hot spots and your hardest migrations will live. The single-engine mechanics live in the databases track’s sharding unit; the distributed-transaction patterns in the distributed track’s sagas unit. Now when you see a query fan out to every shard in production, you’ll know whether the partition key was the wrong choice from day one — or whether a scatter-gather is the right deliberate design.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.