System Design Masterclass · Module 05 / Week 1

Partitioning & Sharding

Splitting data across nodes without splitting correctness: partition strategies, consistent hashing and why mod-N is a trap, rebalancing, the hot-partition (celebrity) problem, and what sharding does to your queries.

~3h study~2h exercise2 interactive simsFast track ~80 min

Fast track — sharded before?

Compresses to ~80 min. Strategy definitions collapse; consistent hashing mechanics, rebalancing, and hot partitions stay mandatory — hot keys during live events are your platform's specific nightmare.

  1. §01 takeaway · 5 min
  2. §02 consistent hashing + resharding sim · 20 min · MANDATORY
  3. §03 rebalancing, in full · 15 min · MANDATORY
  4. §04 hot partitions + sim, in full · 20 min · MANDATORY
  5. §05–§06 takeaways; Exercise step 2 · 25 min
STEP 01

Partitioning strategies

SKIM
Fast-track takeawayRange: sorted key ranges per shard — efficient range scans, but sequential keys (timestamps!) write to one shard, creating a rolling hot spot. Hash: uniform spread, range queries destroyed. Directory: lookup service maps key→shard — maximal flexibility, one more thing to keep consistent and available. Secondary indexes: local (per-shard; reads scatter-gather every shard) vs global (index itself sharded by indexed value; reads targeted, writes now touch multiple shards). The partition key decision is the schema decision — everything else is downstream of it.
StrategyWinsLosesUsed by
Rangerange scans, sorted iterationhot spots on sequential keys (time!)HBase, Bigtable, Mongo range
Hashuniform distributionrange queries → scatter-gatherCassandra, DynamoDB, Redis Cluster
Directoryarbitrary placement, easy migrationthe directory is a dependency + consistency burdenVitess-style, custom estates

Secondary indexes under partitioning: local indexes make every index read a fan-out to all shards (tail amplification, Module 02); global indexes make reads targeted but turn one logical write into multi-shard writes. Pick per access pattern.

STEP 02

Consistent hashing — why mod-N is a trap

MANDATORY

shard = hash(key) % N looks obviously right and fails catastrophically at the worst moment: change N and almost every key remaps — a self-inflicted cache avalanche or a full-cluster data migration, triggered by adding capacity. Consistent hashing fixes it: nodes and keys hash onto a ring; a key belongs to the next node clockwise; adding a node steals only ~1/N of the keyspace, from its neighbors alone. Virtual nodes (100–256 tokens per physical node) smooth the statistical imbalance and let heterogeneous hardware take proportional load.

Resharding cost · 100 keys, add a 5th node
KEYS REMAPPED
OF 100
Each square is a key, colored by owning node (4 nodes). Add a 5th node with each scheme and watch how many keys move (highlighted).
The number to remembermod-N remaps ~N/(N+1) of all keys (~80% going 4→5 nodes). Consistent hashing remaps ~1/(N+1) (~20%). At cluster scale that's the difference between a non-event and an outage. Every serious system — Cassandra, DynamoDB, Redis Cluster (via its slot layer), Memcached clients — uses ring-or-slots for exactly this reason.
STEP 03

Rebalancing in real systems

MANDATORY

Production systems mostly don't hash straight to nodes — they hash to a fixed pool of partitions, then map partitions → nodes. Rebalancing becomes moving whole partitions, and the key→partition mapping never changes:

Rules regardless of system: rebalancing must be throttled (moving data competes with serving it — untuned rebalancing is a self-inflicted incident), resumable, and never automatic on mere node-unreachability — a GC pause misread as node death triggers a data-movement storm that causes the real outage.

STEP 04

Hot partitions — the celebrity problem

MANDATORY

Perfect key distribution ≠ perfect load distribution: keys aren't equally popular. One live-final manifest, one trending series — Zipfian reality means one shard melts while others idle. Salting spreads a hot key across shards at the cost of read-side aggregation:

Hot shard · Zipfian load, key "match:final" on shard 3
Shard 3 at ~62% of total load — it saturates while five shards nap. Salt the hot key (match:final:{0..7}) and watch it flatten.
Staff expectationChoosing the partition key against a written list of the top access patterns and known skew, and pre-declaring the hot-event playbook: which keys get pre-salted, what per-slot QPS triggers it, who owns the read-merge. "user_id is a natural key" is a mid-level answer; "user_id, except device-session data co-located via hash tags, plus a pre-salting playbook for event-day content keys" is the staff one.
STEP 05

What sharding does to queries

SKIM
Fast-track takeawaySingle-key ops stay O(1); everything else gets a tax. Cross-shard queries → scatter-gather (latency = slowest shard: tail amplification again); cross-shard JOINs → don't — denormalize at write time or co-locate by shared partition key; cross-shard transactions → 2PC is slow and fragile, the microservices answer is the Saga (Week 3); cross-shard uniqueness → dedicated keyspace or ID-generation scheme (Snowflake IDs). Design rule: choose the partition key so your top-N queries are single-shard, and treat every scatter-gather as a design smell to justify.
STEP 06

Code: a ring you can reason about

SKIM
Fast-track takeawayThe whole algorithm is a TreeMap and two methods: hash each node to many ring positions (vnodes), and lookup = ceilingEntry(hash(key)) with wraparound. Skim it once so "consistent hashing" is 25 lines in your head, not a paper. Note the Redis hash-tag snippet below it — co-location you'll actually use this quarter.
public class ConsistentHashRing<N> {
  private final TreeMap<Long, N> ring = new TreeMap<>();
  private final int vnodes;                       // 100–256 in practice

  public void addNode(N node) {
    for (int v = 0; v < vnodes; v++)
      ring.put(hash(node + "#" + v), node);      // murmur3/xxHash — NOT hashCode()
  }
  public void removeNode(N node) {
    for (int v = 0; v < vnodes; v++) ring.remove(hash(node + "#" + v));
  }
  public N nodeFor(String key) {
    var e = ring.ceilingEntry(hash(key));          // next node clockwise…
    return (e != null ? e : ring.firstEntry()).getValue(); // …wrapping the ring
  }
}
// Redis Cluster co-location: hash tags force one slot → multi-key ops legal
redis.opsForValue().multiSet(Map.of(
    "{user:42}:profile",  profileJson,
    "{user:42}:sessions", sessionsJson));   // same slot: only {user:42} is hashed
STEP 07

Exercise: measure the remap, melt a shard

MANDATORY

Fast trackStep 2 only (~40 min): building the ring and measuring 80% vs 20% remap with your own code makes the argument permanent.

1
Build the ring. Implement ConsistentHashRing with pluggable vnode count, using Guava's murmur3. Unit-test distribution: 1M keys over 5 nodes should land within ±5% of 200k each at 200 vnodes.
2
Measure the remap. Assign 1M keys via mod-5, then mod-6: count movers. Repeat with the ring 5→6 nodes. Produce the two percentages (~83% vs ~17%) from your own run, and additionally chart imbalance vs vnodes ∈ {1, 10, 100, 500}.
3
Melt and salt. Local 3-node Redis Cluster (docker). Zipfian load-test with one dominant key; watch per-node QPS. Apply read-replication salting ×8 in your client; show per-node QPS flatten. Keep before/after screenshots.
4
Design on paper. Shard your watch-history store: pick a partition key against these queries — (a) resume point by (user, content), (b) continue-watching rail by user, (c) analytics "views per content per day". State which query becomes scatter-gather and what you denormalize to avoid it.

Self-check

Why does adding a node to a mod-N cluster cause a cache avalanche?
~N/(N+1) of keys change owner instantly, so nearly every lookup goes to a node that's never seen that key — cluster-wide hit ratio collapses toward zero at once, and the full miss load lands on the origin: Module 01's avalanche, triggered by adding capacity.
What do vnodes buy beyond smoothing randomness?
(1) A leaving node's load spreads across many successors instead of one neighbor eating it all. (2) Heterogeneous hardware: give a 2× machine 2× vnodes. (3) Rebalancing granularity — move small slices, throttled, instead of a monolithic range.
Why is Kafka partition count effectively permanent for keyed topics?
Key→partition is hash(key) % partitions. Changing the count remaps keys to different partitions, so a key's events straddle two partitions across the change — per-key ordering, the property keyed topics exist for, silently breaks. Hence day-one over-provisioning.
Timestamp-prefixed keys on range partitioning — what happens and what are two fixes?
All current writes hit the shard owning "now" — a rolling hot spot that moves but never spreads. Fixes: prefix with a hash/entity component (hash(sensor)‖ts) to spread writes while keeping per-entity time locality; or bucket time into salted windows and merge on read. Pure timestamp keys are the classic range-sharding own-goal.