Chapter 30: Consistent Hashing & Data Placement
Introduction
Consistent hashing is the foundational technique for horizontal data partitioning in distributed systems. It solves a fundamental problem: how to distribute data across N servers such that adding or removing servers doesn't force a complete reshuffle.
This chapter covers:
- Why naive hashing (
hash(key) % N) fails catastrophically when N changes - Consistent hashing's hash ring concept and virtual nodes
- Modern variants: bounded-load, jump hash, rendezvous, Maglev
- Real implementations in DynamoDB, Cassandra, Memcached, Nginx
- Performance trade-offs and implementation details
Strong candidates explain why consistent hashing minimizes data movement (only K/N keys move) and can sketch the ring with virtual nodes. Weak candidates memorize "hash ring" without understanding redistribution mechanics or the virtual node problem.
1. The Problem: Naive Hashing
1.1 Naive Approach: Modulo Hashing
The obvious approach to distribute keys across N servers:
This works perfectly until N changes. When you add or remove a server, nearly every key remaps to a different server.
1.2 Catastrophic Redistribution
| Key | Hash | N=3 (Before) | N=4 (After) | Moved? |
|---|---|---|---|---|
| user:123 | 0x7A3F | 0x7A3F % 3 = 0 | 0x7A3F % 4 = 3 | YES |
| user:456 | 0x5C12 | 0x5C12 % 3 = 2 | 0x5C12 % 4 = 2 | NO |
| user:789 | 0x9A81 | 0x9A81 % 3 = 1 | 0x9A81 % 4 = 1 | NO |
| user:012 | 0x2E4B | 0x2E4B % 3 = 1 | 0x2E4B % 4 = 3 | YES |
| user:345 | 0xB7D9 | 0xB7D9 % 3 = 2 | 0xB7D9 % 4 = 1 | YES |
Result: Adding 1 server (N=3 → N=4) remapped 3 out of 5 keys (60%). In reality, with millions of keys, ~75% of all data must move.
hash%3=0] --> S0A[Server 0] K2[Key B
hash%3=1] --> S1A[Server 1] K3[Key C
hash%3=2] --> S2A[Server 2] K4[Key D
hash%3=0] --> S0A K5[Key E
hash%3=1] --> S1A end subgraph "After: N=4" K1B[Key A
hash%4=3] -.moved.-> S3B[Server 3] K2B[Key B
hash%4=1] --> S1B[Server 1] K3B[Key C
hash%4=2] --> S2B[Server 2] K4B[Key D
hash%4=0] -.moved.-> S0B[Server 0] K5B[Key E
hash%4=1] --> S1B end
1.3 Why This Matters
- Cache systems: All cache entries invalidate → thundering herd on backend
- Data stores: Massive data movement, network saturation, hours of downtime
- Elastic scaling: Can't add capacity without destroying performance
Modulo hashing creates a global dependency between the hash value and the number of servers. Changing N invalidates the entire mapping.
2. Consistent Hashing Concept
2.1 The Hash Ring
Consistent hashing (Karger et al., 1997) maps both keys and nodes to the same hash space, forming a conceptual ring.
Both keys and nodes hash to positions on this ring
Key idea: Each key is assigned to the first node encountered when traveling clockwise from the key's position on the ring.
hash(A)=100M"] N1["Node B
hash(B)=1.5B"] N2["Node C
hash(C)=3B"] K0["Key X → Node A"] K1["Key Y → Node B"] K2["Key Z → Node C"] style N0 fill:#34d399 style N1 fill:#60a5fa style N2 fill:#a78bfa
2.2 Key Mapping Algorithm
function get_node(key, nodes):
key_hash = hash(key)
# Find first node clockwise from key_hash
for node in sorted(nodes, key=lambda n: hash(n)):
if hash(node) >= key_hash:
return node
# Wrap around: return first node
return sorted(nodes, key=lambda n: hash(n))[0]
2.3 Why This Works
| Property | Naive Hash | Consistent Hash |
|---|---|---|
| Add node | ~K × (N-1)/N keys move | ~K/N keys move |
| Remove node | ~K × (N-1)/N keys move | ~K/N keys move |
| Load distribution | Uniform if N doesn't change | Uneven with few nodes |
| Lookup cost | O(1) | O(log N) binary search |
When asked "how many keys move when you add a server?", walk through the math: with K keys and N nodes, adding 1 node means ~K/N keys in the new node's clockwise arc must move. Contrast with naive hashing where K×(N/(N+1)) keys stay in place but the rest move.
3. Virtual Nodes
3.1 Why Virtual Nodes
Basic consistent hashing has a fatal flaw: with few physical nodes, the ring positions are uneven, causing severe load imbalance.
| Scenario | Node A Range | Node B Range | Node C Range | Imbalance |
|---|---|---|---|---|
| Physical nodes only | 45% of ring | 30% of ring | 25% of ring | 1.8× difference |
| V=10 virtual nodes | 34% of ring | 33% of ring | 33% of ring | 1.03× difference |
| V=100 virtual nodes | 33.4% of ring | 33.2% of ring | 33.4% of ring | 1.006× difference |
Solution: Each physical node is represented by V virtual nodes at different positions on the ring.
for i in [0, V)
pos=50M"] VA2["A:1
pos=1.2B"] VA3["A:2
pos=2.8B"] VB1["B:0
pos=300M"] VB2["B:1
pos=1.9B"] VB3["B:2
pos=3.5B"] end PA -.owns.-> VA1 PA -.owns.-> VA2 PA -.owns.-> VA3 PB -.owns.-> VB1 PB -.owns.-> VB2 PB -.owns.-> VB3 style PA fill:#34d399 style PB fill:#60a5fa
3.2 Tuning V
| V Value | Load Balance | Memory Cost | Lookup Cost | Use Case |
|---|---|---|---|---|
| 1 | Poor | Minimal | O(log N) | Not recommended |
| 10-50 | Acceptable | Low | O(log N×V) | Small clusters (N < 10) |
| 100-200 | Good | Moderate | O(log N×V) | Medium clusters (10-100 nodes) |
| 500+ | Excellent | High | O(log N×V) | Large clusters (rare, use alternatives) |
Cassandra default: V=256 (historically), V=16 in modern versions
DynamoDB: Uses virtual nodes internally, not exposed to users
Higher V improves load balance but increases memory (must store V×N positions) and lookup time (binary search over V×N entries). Practical sweet spot: V=100-200 for most systems.
4. Adding/Removing Nodes
4.1 Redistribution Mechanics
When a new node joins, it claims responsibility for a fraction of the key space. Only keys in its clockwise arc must move.
4.2 Mathematical Proof
Proof:
- New node D claims 1/(N+1) of the ring on average
- Keys uniformly distributed → K/(N+1) keys in that arc
- Those keys previously belonged to D's clockwise successor
- Only those keys move; all others stay put
| Operation | Keys Moved | Example (K=1M, N=10) |
|---|---|---|
| Add node (naive) | K × N/(N+1) ≈ 909K | 90.9% of data moves |
| Add node (consistent) | K / (N+1) ≈ 91K | 9.1% of data moves |
| Remove node (naive) | K × (N-1)/N ≈ 900K | 90% of data moves |
| Remove node (consistent) | K / N = 100K | 10% of data moves |
4.3 Minimal Disruption Guarantee
Consistent hashing provides a monotonicity property:
- Adding a node only causes keys to move to the new node
- Removing a node only causes its keys to move to its successor
- Keys never shuffle between existing nodes
Q: "Why is consistent hashing better than mod-N?"
A: "With mod-N, changing N invalidates almost all mappings because the modulo creates a global dependency. Consistent hashing decouples nodes from each other—each node owns an arc of the ring, and changes only affect neighboring arcs. This reduces data movement from O(K) to O(K/N)."
5. Bounded-Load Consistent Hashing
Standard consistent hashing has no protection against hot spots—if keys skew to one arc, a single node becomes overloaded.
5.1 The Problem
| Scenario | Node A Load | Node B Load | Node C Load | Issue |
|---|---|---|---|---|
| Uniform key distribution | 33% | 33% | 34% | Balanced |
| Popular key "user:celebrity" | 70% | 15% | 15% | A overloaded |
5.2 Bounded-Load Solution (Mirrokni et al., Google 2018)
Set a load cap for each node:
Typical ε = 0.25 (25% over average)
Algorithm:
- Start with standard consistent hash lookup
- If target node is at max capacity, skip to next clockwise node
- Repeat until finding a node under capacity
- Track load in a separate data structure (not the ring)
function get_node_bounded(key, nodes, load_map, epsilon):
key_hash = hash(key)
avg_load = total_keys / len(nodes)
max_load = (1 + epsilon) * avg_load
candidates = sorted(nodes, key=lambda n: hash(n) if hash(n) >= key_hash
else hash(n) + 2^32)
for node in candidates:
if load_map[node] < max_load:
return node
# Fallback: return least loaded
return min(nodes, key=lambda n: load_map[n])
5.3 Performance Impact
| ε Value | Max Overload | Lookup Cost | Balance Quality |
|---|---|---|---|
| 0 (no bound) | Unlimited | O(log N) | Poor under skew |
| 0.25 | +25% | O(log N) avg | Good |
| 0.1 | +10% | O(N) worst case | Excellent |
Google: Uses bounded-load consistent hashing for Cloud Storage and Bigtable load balancing. Cloudflare: Adopted for edge server load distribution.
6. Jump Consistent Hash
Jump Consistent Hash (Google, 2014) is a radically simpler algorithm that eliminates the hash ring entirely.
6.1 Algorithm Deep Dive
// C++ reference implementation (Lamping & Veach, Google 2014)
int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {
int64_t b = -1, j = 0;
while (j < num_buckets) {
b = j;
key = key * 2862933555777941757ULL + 1;
j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));
}
return b;
}
How it works:
- Starts with bucket -1, then "jumps" forward based on pseudorandom values derived from the key
- Each iteration computes: "if we had j+1 buckets, would this key still be in bucket b?"
- Loop terminates when j reaches num_buckets
- Returns the last valid bucket index
6.2 Properties
| Property | Jump Hash | Consistent Hash Ring |
|---|---|---|
| Memory | O(1) - no data structure | O(N×V) - store ring positions |
| Lookup time | O(log N) expected jumps | O(log N×V) binary search |
| Balance | Perfect (no virtual nodes needed) | Depends on V |
| Add bucket | Exactly K/(N+1) keys move | ~K/(N+1) keys move |
| Remove arbitrary bucket | NOT SUPPORTED | Supported |
6.3 Limitations
Jump hash requires sequential bucket IDs [0, N-1]. You cannot remove an arbitrary bucket—only the last one. This makes it unsuitable for systems where nodes have persistent identities (like server hostnames) or where any node can fail.
6.4 Use Cases
- Good: Client-side sharding where you control bucket count (e.g., 16 shards)
- Good: Static partitioning (e.g., partition log files across 100 buckets)
- Bad: Distributed caches where servers come and go
- Bad: Systems needing heterogeneous node weights
If asked about jump hash, immediately clarify: "It's O(1) memory and perfectly balanced, but it only works for sequential buckets—you can't remove arbitrary nodes. It's great for static partitioning but not for elastic clusters."
7. Rendezvous Hashing (Highest Random Weight)
Rendezvous hashing (Thaler & Ravishankar, 1998) takes a completely different approach: for each key, compute a score for every node and pick the highest.
7.1 Algorithm
function rendezvous_hash(key, nodes):
max_score = -1
selected_node = None
for node in nodes:
score = hash(key + node.id) # Combine key and node
if score > max_score:
max_score = score
selected_node = node
return selected_node
Key insight: Since the score depends on both key and node, adding/removing a node only affects keys where that node had the highest score.
7.2 Comparison
| Aspect | Rendezvous | Consistent Hash Ring |
|---|---|---|
| Lookup time | O(N) - check all nodes | O(log N×V) |
| Memory | O(N) - just node list | O(N×V) - ring positions |
| Balance | Perfect (no virtual nodes) | Depends on V |
| Add/remove any node | Supported | Supported |
| Weighted nodes | Easy: duplicate in list | Adjust V per node |
7.3 Optimization: Top-K Candidates
For replication, rendezvous naturally extends to "pick top K nodes by score":
function rendezvous_top_k(key, nodes, k):
scores = [(node, hash(key + node.id)) for node in nodes]
scores.sort(key=lambda x: x[1], reverse=True)
return [node for node, score in scores[:k]]
7.4 Use Cases
- Small N: When you have 10-100 nodes, O(N) lookup is acceptable
- Weighted nodes: Easy to give powerful nodes more replicas
- Simple implementation: No complex ring data structure
Tahoe-LAFS: Uses rendezvous for replica placement. GlusterFS: Originally used consistent hashing, later added rendezvous option for better balance.
8. Maglev Hashing
Maglev (Google, 2016) is designed for high-performance load balancers. It pre-computes a lookup table for O(1) lookups with minimal disruption.
8.1 Concept
Instead of a hash ring, Maglev builds a fixed-size table where each entry points to a backend. Keys hash to table indices.
M = table size (typically 65537, a prime)
8.2 Table Population Algorithm
function populate_maglev_table(backends, M):
table = [None] * M
# Generate permutation for each backend
permutations = {}
for backend in backends:
offset = hash1(backend) % M
skip = hash2(backend) % (M - 1) + 1
perm = [(offset + i * skip) % M for i in range(M)]
permutations[backend] = perm
# Fill table round-robin
indices = {b: 0 for b in backends}
filled = 0
while filled < M:
for backend in backends:
idx = indices[backend]
while idx < M:
pos = permutations[backend][idx]
indices[backend] += 1
idx += 1
if table[pos] is None:
table[pos] = backend
filled += 1
break
return table
Properties:
- Each backend gets ~M/N entries in the table
- Backends are evenly distributed (no virtual nodes needed)
- When a backend is added/removed, only ~1/N entries change
8.3 Performance
| Operation | Time Complexity | Notes |
|---|---|---|
| Lookup | O(1) | Just hash + array access |
| Build table | O(M × N) | Done rarely (on config change) |
| Memory | O(M) | M = 65537 → ~64KB for IDs |
8.4 Connection Disruption
| Algorithm | Connections Disrupted (Add 1/N nodes) |
|---|---|
| Naive hash | ~100% × N/(N+1) |
| Consistent hash (V=100) | ~1/(N+1) worst case |
| Maglev | ~1/(N+1) guaranteed |
Google's Maglev load balancer serves millions of requests per second with sub-millisecond latency. The lookup table fits in CPU cache (L2/L3), making lookups extremely fast. Used for Google Cloud Load Balancing and YouTube traffic.
9. Real Systems
9.1 DynamoDB
Architecture:
- Partition key → MD5 hash → 128-bit value → maps to partition
- Consistent hashing with virtual nodes (internal, not exposed)
- Each partition ~10GB, splits automatically when exceeded
// DynamoDB partition assignment (conceptual)
partition_key = "user:12345"
hash_value = MD5(partition_key) // 128-bit
partition = ring.find_node(hash_value)
// Replication: N=3 replicas across AZs
replicas = [partition, partition.successor, partition.successor.successor]
'user:12345'] --> H[MD5 Hash
0x7A3F...8B2C] H --> R[Hash Ring
Lookup] R --> N1[Node A
Primary] R --> N2[Node B
Replica 1] R --> N3[Node C
Replica 2] style N1 fill:#34d399 style N2 fill:#60a5fa style N3 fill:#a78bfa
9.2 Cassandra
Architecture:
- Token ring: each node claims a range of tokens (virtual nodes)
- Default: V=256 (older) or V=16 (3.0+, tunable)
- Partition key → Murmur3 hash → token → node
- Replication factor (RF) determines how many nodes store each key
| Setting | Value | Impact |
|---|---|---|
num_tokens |
16 (default) | Number of virtual nodes per physical node |
partitioner |
Murmur3Partitioner | Hash function (128-bit output) |
replication_factor |
3 (typical) | How many nodes store each partition |
// Cassandra token ring (simplified)
CREATE KEYSPACE my_keyspace WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3
};
// Key lookup
key = "user:12345"
token = murmur3(key) % 2^63 // Signed 64-bit range
coordinator = token_ring.find_node(token)
replicas = coordinator.successors(RF=3)
9.3 Memcached
Client-side consistent hashing: Memcached server has no knowledge of the ring—clients implement it.
// libmemcached consistent hashing (C)
memcached_st *memc = memcached_create(NULL);
memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_DISTRIBUTION,
MEMCACHED_DISTRIBUTION_CONSISTENT_KETAMA);
// Ketama algorithm (default): MD5 hash with 160 virtual nodes per server
// Client computes: hash(key) → binary search on ring → server
Ketama algorithm:
- Each server gets 160 virtual nodes (40 hashes × 4 positions each)
- MD5 hash:
hash(server_address + ":" + i)for i in [0, 40) - Take 4 × 32-bit slices from each 128-bit MD5 output
9.4 Nginx
Upstream consistent hash directive:
upstream backend {
hash $request_uri consistent;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
# Hash the request URI, use consistent hashing to pick server
# Minimizes cache misses when backend servers change
Virtual nodes: Nginx uses 160 virtual nodes per server by default (same as Ketama).
Virtual Nodes] end subgraph "Cassandra" C1[Partition Key] --> C2[Murmur3] --> C3[Token Ring
V=16 default] end subgraph "Memcached" M1[Key] --> M2[Client: MD5] --> M3[Ketama Ring
V=160] end subgraph "Nginx" N1[Request URI] --> N2[Hash] --> N3[Upstream Ring
V=160] end
10. Algorithm Comparison
| Algorithm | Memory | Lookup | Add Node | Remove Any Node | Balance | Best Use Case |
|---|---|---|---|---|---|---|
| Naive Hash | O(1) | O(1) | O(K) keys move | O(K) keys move | Perfect | Fixed cluster size |
| Consistent Hash | O(N×V) | O(log N×V) | O(K/N) keys | O(K/N) keys | Depends on V | Elastic clusters, caches |
| Bounded-Load CH | O(N×V) | O(N) worst | O(K/N) keys | O(K/N) keys | Excellent | Skewed workloads |
| Jump Hash | O(1) | O(log N) | O(K/N) keys | Only last bucket | Perfect | Static partitioning |
| Rendezvous | O(N) | O(N) | O(K/N) keys | O(K/N) keys | Perfect | Small N, weighted nodes |
| Maglev | O(M) | O(1) | O(K/N) keys | O(K/N) keys | Excellent | Load balancers |
Decision Matrix
| Requirement | Recommended Algorithm |
|---|---|
| Need to remove arbitrary nodes | Consistent Hash, Rendezvous, Maglev |
| Minimize memory (millions of keys) | Jump Hash, Rendezvous |
| Minimize lookup latency | Maglev, Naive Hash |
| Handle hot keys | Bounded-Load Consistent Hash |
| Static partition count (no elasticity) | Jump Hash |
| Simple implementation | Rendezvous (if N small) |
| Heterogeneous node weights | Rendezvous, Consistent Hash |
11. Implementation Considerations
11.1 Hash Function Choice
| Hash Function | Output Size | Speed | Collision Resistance | Use Case |
|---|---|---|---|---|
| MD5 | 128 bits | Moderate | Broken (crypto) | DynamoDB, Memcached (legacy) |
| MurmurHash3 | 32/128 bits | Fast | Good (non-crypto) | Cassandra, Redis |
| xxHash | 32/64/128 bits | Very Fast | Excellent | Modern systems |
| SHA-1 | 160 bits | Slow | Broken (crypto) | Avoid |
| CRC32 | 32 bits | Very Fast | Poor | Checksums only |
Recommendation: Use xxHash for new implementations (fast, good distribution). Use MurmurHash3 for compatibility with Cassandra/Redis ecosystems.
11.2 Replication with Consistent Hashing
Store data on the primary node and its N-1 clockwise successors:
function get_replica_nodes(key, ring, replication_factor):
primary = ring.find_node(hash(key))
replicas = [primary]
current = primary
while len(replicas) < replication_factor:
current = ring.successor(current)
# Skip if same physical node (virtual nodes)
if current.physical_id not in [r.physical_id for r in replicas]:
replicas.append(current)
return replicas
Rack/Zone Awareness:
- Cassandra:
NetworkTopologyStrategyensures replicas in different racks/DCs - DynamoDB: Replicas always in different Availability Zones
11.3 Preference Lists
DynamoDB-style preference list: pre-compute N replicas for fast quorum operations.
// Preference list: ordered list of nodes for a key
class PreferenceList {
primary: Node
replicas: Node[] // Length = N
function quorum_write(key, value, W):
responses = []
for node in replicas:
responses.append(node.write_async(key, value))
# Wait for W successes
return wait_for_n(responses, W)
function quorum_read(key, R):
responses = []
for node in replicas:
responses.append(node.read_async(key))
# Wait for R responses, resolve conflicts
results = wait_for_n(responses, R)
return resolve_conflicts(results)
}
11.4 Handling Node Failures
| Scenario | Solution |
|---|---|
| Node temporarily down | Hinted handoff: buffer writes at successor, replay when node returns |
| Node permanently removed | Remove from ring, keys redistribute to successor |
| Node replaced (new hardware) | Assign same token/position on ring, restore data from replicas |
| Network partition | Sloppy quorum: accept writes at any N healthy nodes, not just preference list |
11.5 Code Example: Consistent Hash Ring
class ConsistentHashRing:
def __init__(self, nodes, virtual_nodes=100):
self.virtual_nodes = virtual_nodes
self.ring = {} # position -> node_id
self.sorted_positions = []
for node in nodes:
self.add_node(node)
def add_node(self, node):
for i in range(self.virtual_nodes):
vnode_key = f"{node}:{i}"
position = xxhash.xxh64(vnode_key).intdigest()
self.ring[position] = node
self.sorted_positions = sorted(self.ring.keys())
def remove_node(self, node):
for i in range(self.virtual_nodes):
vnode_key = f"{node}:{i}"
position = xxhash.xxh64(vnode_key).intdigest()
del self.ring[position]
self.sorted_positions = sorted(self.ring.keys())
def get_node(self, key):
if not self.ring:
return None
position = xxhash.xxh64(key).intdigest()
# Binary search for first node clockwise
idx = bisect.bisect_right(self.sorted_positions, position)
if idx == len(self.sorted_positions):
idx = 0 # Wrap around
return self.ring[self.sorted_positions[idx]]
def get_nodes(self, key, n):
"""Get n distinct physical nodes for replication"""
if not self.ring:
return []
position = xxhash.xxh64(key).intdigest()
idx = bisect.bisect_right(self.sorted_positions, position)
nodes = []
seen = set()
count = 0
while len(nodes) < n and count < len(self.sorted_positions):
if idx >= len(self.sorted_positions):
idx = 0
node = self.ring[self.sorted_positions[idx]]
if node not in seen:
nodes.append(node)
seen.add(node)
idx += 1
count += 1
return nodes
Q: "Implement a consistent hash ring with add/remove node and get_node methods."
Key points:
- Use a sorted list of positions for binary search (O(log N×V) lookup)
- Virtual nodes: hash
node_id + ":" + vnode_index - Handle wrap-around (if search passes end of ring, return first node)
- For replication, track physical nodes to avoid duplicates
12. Interview Signals
Strong Signals
- Explains why naive hashing fails: "Modulo creates a global dependency on N—changing N remaps almost all keys"
- Quantifies data movement: "Consistent hashing moves only K/N keys vs K×(N-1)/N for naive"
- Mentions virtual nodes unprompted: "With few nodes, you need virtual nodes to balance the ring"
- Knows alternative algorithms: "Jump hash is O(1) memory but can't remove arbitrary nodes"
- Discusses real systems: "Cassandra uses Murmur3 with 16 vnodes by default, down from 256"
- Addresses hot keys: "Standard consistent hashing doesn't prevent hot spots—you need bounded-load or sharding"
Weak Signals
- Only memorizes "hash ring" without understanding redistribution math
- Can't explain why virtual nodes are needed
- Doesn't know any real system implementations
- Confuses consistent hashing with quorum consensus
- Can't compare consistent hash vs jump hash vs rendezvous
Common Mistakes
Wrong: Without virtual nodes, balance can be very poor (2x-3x difference between nodes).
Right: "You need V=100-200 virtual nodes per physical node to get good balance."
Wrong: If one key is extremely popular, its node will be overloaded.
Right: "Consistent hashing distributes keys evenly across the ring, but doesn't handle skewed access patterns. You need bounded-load hashing or application-level sharding for hot keys."
Wrong: Jump hash can't remove arbitrary nodes.
Right: "Jump hash is great for static partitioning where you control the bucket count, but it doesn't work for elastic clusters where any node can fail."
Sample Interview Questions
Q1: Design a distributed cache (Memcached-like) with 100 servers
Answer:
- Client-side consistent hashing: Clients maintain a hash ring with all 100 servers
- Virtual nodes: Each server gets V=100 virtual nodes for balance
- Hash function: xxHash or MurmurHash3 (fast, good distribution)
- Key lookup:
hash(key)→ binary search on ring → server - Add server: Update ring, only ~1/101 of keys need to move (no need to rehash everything)
- Remove server: Remove from ring, keys redistribute to clockwise successor
- Health checking: Clients periodically ping servers, remove dead ones from ring
Q2: Why does DynamoDB use consistent hashing instead of range partitioning?
Answer:
- Range partitioning risk: Sequential keys create hot partitions (e.g., timestamps, auto-incrementing IDs)
- Hash partitioning advantage: Randomizes key distribution, prevents hot spots
- Elastic scaling: Adding partitions doesn't require global coordination (only K/N keys move)
- Trade-off: Can't do range scans across partitions—need to use Global Secondary Indexes
Q3: You add a new server to a consistent hash ring with 10 existing servers. How many keys move, and where do they come from?
Answer:
- Expected keys moved: ~K/11 (where K = total keys)
- Source: Keys come from the new server's clockwise successor on the ring
- Reason: The new server claims an arc of the ring that previously belonged to one other server
- With virtual nodes: Keys come from multiple servers (each vnode steals from its successor), but still ~K/11 total
- Contrast with naive hash: mod-N would remap ~K×10/11 ≈ 909K keys (90.9%)
Q4: When would you use Jump Hash instead of Consistent Hash?
Use Jump Hash when:
- You control the bucket count (e.g., fixed sharding: 16, 32, 64 buckets)
- Buckets are sequential IDs [0, N-1]
- You only add buckets, never remove arbitrary ones
- You want O(1) memory (no ring data structure)
Use Consistent Hash when:
- Nodes have persistent identities (server hostnames, not just IDs)
- Any node can fail or be removed
- You need heterogeneous node weights
- You need rack/zone-aware replica placement
Q5: How does Cassandra handle replication with consistent hashing?
Answer:
- Partition key → token:
token = Murmur3(partition_key) - Primary node: First node clockwise from token on ring
- Replicas: Next RF-1 distinct physical nodes clockwise
- NetworkTopologyStrategy: Ensures replicas in different racks/DCs
- Virtual nodes (num_tokens=16): Each physical node occupies 16 positions on ring
- Coordinator: Any node can coordinate read/write, forwards to replicas
- Quorum:
W=2, R=2with RF=3 → overlap guarantees consistency