Replication gave you read capacity and safety, but it did not solve two things: every write still lands on one leader, and every machine still stores the entire dataset.
When you have 50TB of data, or more writes than one machine can absorb, copying is no longer the answer. You have to split the data itself so each machine owns a slice.
That is partitioning, usually called sharding when the slices live on separate machines.
Diagram, as text
- Application leads to Route by shard key
- Route by shard key leads to Shard 1 (users A-H)
- Route by shard key leads to Shard 2 (users I-P)
- Route by shard key leads to Shard 3 (users Q-Z)
Each shard is a full, independent database holding a subset of rows. Three shards means roughly a third of the data and a third of the write load each.
Choosing the shard key
Every decision in sharding flows from one choice: which column decides where a row lives?
Get it right and the system scales smoothly for years. Get it wrong and you face a migration that is among the most painful in this field.
Range partitioning
Split by ranges of the key: users A–H on shard 1, I–P on shard 2, and so on. Timestamps are a common variant — one shard per month.
Good: range queries are efficient, because "all orders in March" lives on one shard.
Bad: it concentrates load. With time-based ranges, every new write goes to the newest shard while older shards sit idle. You have distributed storage but not write load — often the very thing you were trying to fix.
Hash partitioning
Hash the key and take the remainder: shard = hash(user_id) % num_shards.
Good: distribution is close to uniform, so load spreads evenly. This is the common default.
Bad: ranges are destroyed. Sequential ids scatter across all shards, so "all orders from March" must query every shard and merge results.
Worse: changing the shard count remaps almost everything. Going from 4 shards to 5 changes hash % n for roughly 80% of keys, meaning a mass data migration. That specific problem is what the next lesson solves.
Directory-based
Keep an explicit lookup table mapping keys to shards. Maximum flexibility — you can move an individual customer to a dedicated shard. The cost is a lookup on every query and a directory that becomes a single point of failure and a bottleneck.
Hot spots: the failure that defines the topic
Uniform distribution is the goal, and real data is rarely uniform.
Shard by country and India and the United States get enormous shards while Luxembourg gets a tiny one. Shard a social network by user_id and it looks fine — until one celebrity with 50 million followers makes their shard the busiest machine in the fleet.
A hot shard is a serious problem: it becomes the bottleneck for the whole system, and you cannot fix it by adding shards, because the hot key still lands on exactly one.
Mitigations, roughly in order of preference:
- Choose a higher-cardinality key. Shard by
user_idrather thancountry. - Add a compound key. For a hot celebrity, shard by
(user_id, post_id)so their data spreads. - Salt the key for known-hot entities: append a random suffix from a small range, turning one key into ten.
- Handle hot entities separately — cache them aggressively, or give them dedicated capacity.
What sharding makes harder
This is the part beginners underestimate, and the part interviewers want to hear. Sharding is not free capacity — it is capacity bought with capability.
Joins across shards stop working. If users are on shard 1 and their orders on shard 3, the database cannot join them. You fetch from both and combine in application code — slower, more code, and no transactional guarantee.
Transactions across shards become distributed transactions. Two-phase commit is slow and complex; the usual alternative is a saga with compensating actions. Either way, the simple BEGIN … COMMIT you relied on is gone.
Queries without the shard key hit every shard. WHERE email = ? when sharded by user_id must ask all shards and merge — a scatter-gather. This is why the shard key should match your most common access pattern.
Uniqueness across shards is hard. A UNIQUE constraint only applies within one shard, so a globally unique email needs a separate index or a dedicated service. Auto-increment ids collide too, which is why distributed id generators exist.
Rebalancing is a project. Adding shards means moving data while serving traffic.
Operations multiply. Backups, migrations, monitoring and schema changes now happen across N databases, and a migration that half-applies leaves shards with different schemas.
Resharding
You will eventually need more shards, and it is the hardest routine operation in a sharded system.
With naive hash % n, going from 4 to 5 shards remaps most keys. You must copy enormous amounts of data while the system serves traffic, then cut over without losing writes.
Two ways to make it survivable:
Consistent hashing — the next lesson — minimises how many keys move when the shard count changes.
Logical shards. Create many more partitions than machines from the start — say 1,024 logical shards spread over 4 physical servers. Growing means reassigning whole logical shards to a new machine rather than rehashing individual rows. The mapping changes; the data placement within a logical shard does not. This is what most mature systems do, and it is a strong answer in an interview.
When not to shard
Sharding is one of the most consequential decisions you can make, and it is frequently made too early.
Before sharding, exhaust the cheaper options:
- Add indexes and fix slow queries. Most "we need to shard" moments are one missing index.
- Add a cache — most load is reads, and reads are cacheable.
- Add read replicas — as covered previously.
- Scale up. A modern server with a terabyte of RAM handles far more than most teams assume.
- Archive cold data. Often 90% of rows are never queried; move them out.
Shard when you genuinely cannot fit the data on one machine, or cannot absorb the write rate. Not before.
Check your understanding
Common mistakes
Sharding on a low-cardinality column. Country, status or category guarantees uneven shards.
Choosing a key that does not match query patterns. If most queries filter by email but you shard by user id, nearly every query becomes a scatter-gather.
Sharding too early. You accept permanent complexity to solve a problem you do not have yet.
Forgetting cross-shard uniqueness. Discovering that emails are only unique per shard, in production, is a bad day.
No plan for resharding. Deciding on 4 shards with no path to 8 is deferring a much harder problem.
Practice
Design the shard key for a multi-tenant analytics product: 50,000 customers, the largest 1% generating 60% of the data, and queries almost always scoped to one customer over a date range. Justify your choice and name what it makes hard.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Start from the query, then check what the size distribution does to it.
Show a strong answer
Start from the access pattern. Nearly every query is "one customer, one date range", so the shard key must contain the customer — otherwise every query becomes scatter-gather across the fleet.
Naive choice: shard by customer ID. Correct for the query and wrong for the distribution. With the top 1% producing 60% of the data, a hash of customer ID puts several enormous tenants on the same shard by chance, and one shard ends up many times larger and busier than the rest. The size skew is the real problem, not the query.
Better: a composite key of customer plus time bucket — for example customer_id and month. Queries for one customer over a range touch only the buckets in that range, still avoiding a full scatter. And a large tenant's data is spread across many shards rather than concentrated on one, because each month lands independently.
Split the largest tenants out entirely. For the handful producing most of the volume, give them their own dedicated shards. This is not a workaround — at this distribution it is the design. It also gives you an isolation story: one enormous customer's query cannot starve the other 49,999.
What it makes hard:
- Cross-customer queries — "usage across all tenants this month" — are now scatter-gather. Serve those from a separate aggregate rolled up asynchronously, not from the sharded store.
- Rebalancing when a tenant grows. A customer who becomes large mid-year needs their data moved. Plan for tenant migration as a supported operation rather than an incident.
- Queries spanning many months touch many shards. If that is common, widen the bucket to quarters.
Your sharded database needs to go from 8 shards to 12. Explain why this is harder than it sounds with modulo hashing, and describe an approach that avoids the pain.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Work out what fraction of keys change shard when the divisor changes.
Show a strong answer
Why modulo is painful. With hash(key) % 8, changing to % 12 changes the destination of roughly two-thirds of all keys. Every one of those rows must move, and until it has, reads must know whether to look in the old place or the new one. For a large dataset that is hours or days of migration during which the system is in two states at once.
Approach one — consistent hashing. Place shards on a ring so that adding a shard only moves the keys between the new shard and its neighbour: roughly 1/n of the data rather than most of it. Adding a twelfth shard moves about 8% of keys. This is precisely the problem consistent hashing exists to solve.
Approach two — many more logical partitions than physical shards. Fix the key space at, say, 1,024 logical partitions from the start, and map partitions to physical shards through a lookup table. Growing from 8 to 12 shards means reassigning some partitions, and only the data in those partitions moves. No key ever changes its partition, so the mapping stays simple and the migration is a bulk copy of whole partitions.
The second is usually preferable when you control the design from the beginning: it makes rebalancing a table update plus a data copy, keeps the hash function fixed forever, and lets you move a hot partition deliberately rather than hoping the ring distributes well. Its cost is one extra indirection on every request, and having to choose the partition count early — too few limits growth, too many adds overhead.
Key takeaways
- Sharding splits data across machines to scale writes and storage — the two things replication cannot do. Real systems shard and replicate.
- The shard key is the decision that matters: range keeps range queries but concentrates load; hash distributes evenly but destroys ranges and complicates resharding.
- Hot spots are the defining failure, and adding shards does not fix them, because one key still maps to one shard. Change the key's distribution instead.
- Sharding costs you joins, transactions, cross-shard uniqueness, and queries that lack the shard key.
- Use many logical shards over few physical machines so growth is reassignment, not rehashing.
- Exhaust indexes, caching, replicas, scaling up and archiving first. Shard when the data genuinely will not fit or the writes will not land.
Next: the specific technique that makes adding and removing nodes cheap, instead of a full reshuffle.
Further reading
- Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017), chapter 6 — partitioning, rebalancing and secondary indexes.
- Giuseppe DeCandia et al., "Dynamo: Amazon's Highly Available Key-value Store" (SOSP 2007) — partitioning with virtual nodes in production.
- Jeff Dean and Sanjay Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (OSDI 2004) — scatter-gather as a first-class model.
- James Corbett et al., "Spanner: Google's Globally-Distributed Database" (OSDI 2012) — automatic resharding and its cost.
- Pat Helland, "Life Beyond Distributed Transactions: An Apostate's Opinion" (CIDR 2007) — why the partition key shapes the whole application.