all_lessons / system_design / 07 · partitioning & sharding lesson 7 / 19

Partitioning & sharding

Caching (lesson 06) bought you read scale by keeping a copy of hot data close. But a cache cannot hold a dataset that no longer fits on one machine, and it does nothing for write throughput. When the authoritative state outgrows a single box, you have no choice left: you split it. This is where distributed systems stop being convenient and start being hard.

1. The first principle: when one box is no longer enough

A single node has three hard ceilings. You partition when you hit any of them:

Partitioning (a.k.a. sharding) splits the dataset into disjoint partitions (shards), each owned by a different node. It buys near-linear scaling of storage and write throughput: N shards → ~N× capacity. It is the orthogonal axis to replication — partitioning splits the data; replication copies each piece. Real systems do both: shard into 64 partitions, then keep 3 replicas of each.

What it costs
Partitioning is not free scaling — it is a trade. Three new problems appear the moment you split:

2. Partitioning schemes

Three families, each a different answer to “given a key, which shard?”

SchemeMappingRange scansLoad spreadMain failure mode
Rangekeys sorted into contiguous ranges (A–F, G–M, …)Excellent — adjacent keys co-locatedPoor — depends on key distributionHot spot: a monotonic key (timestamp, auto-increment id) sends all new writes to the last shard
Hashpartition = hash(key) mod N (or via a ring)Dead — adjacent keys scattered, range = scatter-gather over all shardsExcellent — uniform by constructionKills locality; cannot do efficient ORDER BY / time ranges
Directorya lookup service stores key → shard explicitlyWhatever you encodeTunable — you place data deliberatelyExtra hop on every request; the directory is a SPOF / bottleneck if not itself replicated

Composite strategies combine them: hash a high-level prefix to pick a shard, then range-sort within it (e.g. Cassandra’s (partition_key, clustering_key) — hash for spread across nodes, sort for range scans inside a partition). This recovers in-partition range scans without re-introducing global hot spots.

The resize problem with naive hashing

The obvious hash scheme is shard = hash(key) mod N. It distributes beautifully — until N changes. Add or remove one node and the modulus changes, so almost every key maps somewhere new. When N goes from 4→5, a key’s shard is unchanged only if h mod 4 == h mod 5, which holds for a vanishing fraction. In general changing N reshuffles roughly (N−1)/N of all keys — ~80% at N=5, approaching 100% as N grows.

Make it felt. Take ten keys with hashes 20…29 and recompute their shard when you grow from N=4 to N=5:

key | h mod 4 | h mod 5 | moved? ────┼─────────┼─────────┼─────── 20 | shard 0 | shard 0 | stays 21 | shard 1 | shard 1 | stays 22 | shard 2 | shard 2 | stays 23 | shard 3 | shard 3 | stays 24 | shard 0 | shard 4 | MOVED 25 | shard 1 | shard 0 | MOVED 26 | shard 2 | shard 1 | MOVED 27 | shard 3 | shard 2 | MOVED 28 | shard 0 | shard 3 | MOVED 29 | shard 1 | shard 4 | MOVED

Six of these ten keys land on a different shard. The four that stayed (20–23) are exactly the keys where h mod 4 and h mod 5 happen to coincide — which, because 4 and 5 share no common factor, recurs only once every lcm(4,5)=20 keys (whenever h mod 20 is 0, 1, 2 or 3). Walk a full cycle of 20 keys (0…19) and the count is exactly 16 moved, i.e. 16/20 = 80% = (N−1)/N. So adding one node doesn’t nudge a few keys — it relocates four keys in five. A beginner should picture almost the whole room standing up and swapping seats.

Why a full reshuffle is catastrophic
Reshuffling 90%+ of keys means terabytes of data crossing the network and every cache going cold simultaneously. The cold caches all miss to the backing store at once — a thundering herd that can topple the very database you were trying to grow. A routine “add a node” becomes an outage. This is the single biggest reason naive mod-N is unusable for stateful sharding.

3. Consistent hashing — the centerpiece

The fix: stop tying placement to N. Picture a clock face. The servers stand at a few of the hour-marks, and each data item is also a point on the rim — an item belongs to the next server you reach walking clockwise. Add a server and it drops onto the rim at one spot: only the items between it and the previous server change hands; everyone else keeps the same owner. That is why a circle localises the disruption to ~1/N of keys, where mod-N renumbered the whole room. Formally: map both keys and nodes onto a fixed circular hash space (a ring, e.g. 0…232−1). A key is owned by the first node clockwise from its position. Adding or removing a node only re-homes the keys in the arc between that node and its predecessor — about 1/N of keys, not all of them.

Consistent hash ring (clockwise ownership) 0 / 2^32 | N3 *···|···* N1 / | \ / keyK \ keyK hashes here ──► owned by N1 * · * (first node clockwise) N2 \ / \ / *·····* N4 Add N5 between N4 and N1: ── only keys in arc (N4 → N5] move, from N1 to N5. Every other key stays put. ~1/N remapped.

keys moved when node added ≈ 1/N   vs   mod-N reshuffle ≈ (N−1)/N

Virtual nodes (vnodes): fixing the lumpiness

Plain consistent hashing has two problems. First, with few nodes the random arc lengths are uneven — one node may own a 2×-wide arc and thus 2× the load. Second, removing a node dumps all its load onto its single clockwise neighbour, doubling that node’s burden and often cascading.

The fix is virtual nodes: each physical node is hashed onto the ring at V points (e.g. V=128 or 256). Now a physical node owns 128 scattered small arcs. When it leaves, its load is sprinkled across many neighbours, not dumped on one. And because you’re averaging over V·N random arcs, the load skew shrinks. With more independent random placements landing on each physical node, the lumps average out — the same law-of-large-numbers reason a coin flipped 1000× lands nearer 50% than one flipped 10× — so the relative spread shrinks. The standard deviation of per-node load falls like 1/√V, so:

load skew = max load / avg load ≈ 1 + c / √V   (c ≈ 2–3)

At V=1, skew can be 2× or worse; at V=256, skew is within a few percent of perfectly even. Vnodes also enable heterogeneous hardware: give a node with 2× the RAM 2× the vnodes and it carries proportionally more load automatically.

Worked example — adding a node, 200 shards of data
You run N=4 shards, 200 GB each (800 GB total), and add a 5th node. The takeaway: consistent hashing converts an O(all-data) migration into an O(1/N) ripple. That difference is the whole reason the technique exists.

4. Choosing the shard key

The shard key is the most consequential decision in the design — it’s effectively irreversible at scale (changing it = full re-shard). Three requirements:

  1. High cardinality. Many distinct values, so partitions can be split finely. country has ~200 values → you can never have more than 200 partitions, and the US shard dwarfs the rest. user_id has millions.
  2. Even distribution. No single value should dominate write/read volume.
  3. Query alignment. The dominant query should be answerable from one shard. If you shard by the field your hot path filters on, that read hits a single shard; otherwise it’s scatter-gather.
App: social networkShard by user_idShard by hashtag
Load a user’s timelineSingle shard ✓Scatter-gather ✗
“all posts tagged #X”Scatter-gather ✗Single shard ✓
DistributionEven (millions of users) ✓Skewed — a few tags are huge ✗

There is no key that makes every query single-shard — you optimize for the dominant access pattern and pay scatter-gather (or a secondary index) for the rest.

5. The hot-shard / celebrity problem

Here is the catch a good hash function cannot save you from: skew at the key level. Hashing spreads distinct keys evenly, but if a single key is disproportionately hot — a celebrity with 100M followers, a viral product on launch day — all of its traffic lands on whatever shard owns that one key. No partition scheme distributes the load of one key, because that key lives on one shard by definition.

Mitigations (you will be asked for all three)

6. Secondary indexes & rebalancing

You sharded by the primary key, but queries also filter on other fields. Two ways to index them (for what an index physically is — B-tree vs LSM — see 04):

Local / document-partitionedGlobal / term-partitioned
Index locationCo-located with the data on each shardIndex itself partitioned by the indexed term
Write costCheap — one shard, local updateExpensive — write may touch a different index shard (cross-shard)
Read costScatter-gather — query every shard’s local indexSingle shard — go straight to the term’s index partition
Used byElasticsearch, MongoDB (default), Cassandra SIDynamoDB GSI, search systems wanting fast term lookups

It’s the same write-vs-read trade as everywhere: local indexes make writes cheap and reads fan out; global indexes make reads pinpoint and writes fan out.

Rebalancing without full reshuffles

Even consistent hashing benefits from a deliberate rebalancing strategy. The most robust trick: pick a fixed, large number of partitions up front (e.g. 1024), far more than nodes. Each node owns many partitions; adding a node means moving whole partitions to it, never re-hashing keys. (This is how Riak, Elasticsearch shards, and Kafka-style fixed partition counts work.) Avoid: hash-mod-N (full reshuffle), and unbounded dynamic splitting that thrashes. Rebalancing should move data in the background, throttled, and ideally be operator- or automation-triggered, not a surprise.

Consistent-hashing remap calculator
Start with N nodes; apply a delta (add or remove nodes). Naive mod-N remaps ~(N−1)/N of keys whenever N changes (any change re-mods everything) → near-total reshuffle. Consistent hashing remaps only the arcs touched: ~|delta|/(N+max(delta,0)) of keys. Load skew = max/avg per-node load ≈ 1 + 2.5/√V, dropping as virtual nodes V rise. Tune V and watch the skew vs the remap cost.
mod-N keys remapped
consistent-hash remapped
migration ratio (mod / CH)
load skew (max/avg)
Diagnosis

Interview prompts you should be ready for

  1. Why not just hash(key) mod N? (senior answer) Because changing N re-mods nearly every key — ~(N−1)/N remap on a single add/remove — forcing a full data migration and a simultaneous cache-cold thundering herd. Consistent hashing pins placement to a ring so only ~1/N of keys move; or use a fixed large partition count and move whole partitions.
  2. What do virtual nodes buy you? (senior answer) Two things. They smooth load skew (std-dev of per-node load falls like 1/√V, so skew ≈ 1 + c/√V), and on node removal they spread the departing load across many neighbours instead of dumping it all on one. They also let heterogeneous nodes carry proportional load by owning more vnodes.
  3. How do you pick a shard key? (senior answer) High cardinality, even distribution, and alignment with the dominant query so it hits one shard. I’d name the top query, shard to make it single-shard, and explicitly accept scatter-gather (or a secondary index) for the rest. Avoid low-cardinality keys like country — they cap partition count and create giant uneven shards.
  4. A celebrity user is overloading one shard. Now what? (senior answer) No partition scheme fixes a single hot key — it lives on one shard. Mitigate: split the key with a random suffix to spread writes across sub-keys; cache it aggressively since it’s highly cacheable; or special-case the path (fan-out-on-read for celebrities). I’d combine caching + special-casing.
  5. Local vs global secondary indexes? (senior answer) Local (document-partitioned) indexes sit with the data: cheap writes, but reads scatter-gather across all shards. Global (term-partitioned) indexes are partitioned by the indexed term: reads hit one shard, but writes may touch a different index shard. Pick by read/write ratio of the secondary query.
  6. How is partitioning different from replication? (senior answer) Orthogonal axes. Partitioning splits the dataset into disjoint shards (scales storage + write throughput); replication copies each shard (scales reads + provides fault tolerance). Production systems do both: shard into K partitions, then keep R replicas of each. They’re composed, not alternatives.
  7. How do you rebalance when adding capacity without an outage? (senior answer) Use a fixed large partition count (≫ nodes) and move whole partitions to the new node — no key re-hashing. Throttle the background data copy, keep serving from the old owner until the new replica is caught up, then cut over. Never hash-mod-N.
  8. Estimate the data moved when going from 4 to 5 nodes on an 800 GB dataset. (senior answer) Mod-N: (5−1)/5 = 80% → 640 GB moves plus a cache-cold herd. Consistent hashing: ~1/5 = 20% → 160 GB, the new node claiming its slice. 4× less data and no herd — that gap is exactly why we use the ring.
Takeaway
Partition when one box can’t hold the storage, sustain the write throughput, or keep the index in RAM — and know it costs you scatter-gather reads, hard cross-shard transactions, and a permanent shard-key commitment. Use consistent hashing with many virtual nodes so adding/removing a node remaps ~1/N of keys (not ~all) and load skew stays near 1. Choose the shard key for high cardinality, even spread, and single-shard dominant queries. Remember the one thing partitioning can’t fix: a single hot key — handle celebrities with key-splitting, caching, and special-casing. Next, lesson 08 covers the orthogonal axis: replicating each shard.