Chapter 13: Database Partitioning & Sharding

1. Why Partition/Shard?

The Scalability Wall

Single-node limits:

  • Storage capacity: Dataset exceeds disk capacity (e.g., 10TB data, 2TB disk)
  • Write throughput: Single disk I/O maxed out (e.g., 10K writes/sec needed, 2K capable)
  • Read throughput: CPU/memory saturated serving queries
  • Query performance: Working set doesn't fit in RAM, causing excessive disk I/O

Solution: Split data across multiple nodes (horizontal scaling)

Vertical vs Horizontal Partitioning

Dimension Vertical Partitioning Horizontal Partitioning (Sharding)
What splits Columns/tables (schema) Rows (data)
Example User table β†’ user_profile, user_credentials tables User table β†’ users_0, users_1, users_2 shards
Use case Separate hot/cold data, isolate BLOB columns Scale beyond single node capacity
Complexity Low (app-level joins) High (routing, rebalancing, cross-shard queries)
Scalability gain Limited (still single node per table) Unbounded (add more shards)

Capacity Math Example

Scenario: Social Media User Database

  • Total users: 500M
  • Avg user record: 2KB
  • Total storage: 500M Γ— 2KB = 1TB
  • Write throughput: 50K writes/sec (new users, profile updates)
  • Read throughput: 500K reads/sec

Single node limits:

  • Max disk capacity: 2TB (OK for storage, but no room for indexes/replicas)
  • Max write IOPS: 5K/sec (need 10x more)
  • Max read IOPS: 50K/sec (need 10x more)

Sharding strategy:

Number of shards = max(
  1TB / (2TB Γ— 0.5),  // Storage (50% utilization)
  50K / 5K,            // Write capacity
  500K / 50K           // Read capacity
) = max(1, 10, 10) = 10 shards minimum

With replication (3 replicas): 10 shards Γ— 3 replicas = 30 nodes total

graph LR A[Single Node
1TB data
5K writes/s
50K reads/s] -->|Partition| B[Shard 0
100GB
5K writes/s
50K reads/s] A -->|Partition| C[Shard 1
100GB
5K writes/s
50K reads/s] A -->|Partition| D[Shard 2
100GB
5K writes/s
50K reads/s] A -->|Partition| E[...] A -->|Partition| F[Shard 9
100GB
5K writes/s
50K reads/s] style A fill:#ff6b6b,color:#1a1a2e style B fill:#51cf66 style C fill:#51cf66 style D fill:#51cf66 style F fill:#51cf66

2. Partitioning Strategies

2.1 Range Partitioning

Mechanism: Assign continuous ranges of keys to each partition.

Example: Users A-F β†’ Shard 0, G-M β†’ Shard 1, N-S β†’ Shard 2, T-Z β†’ Shard 3

graph TD subgraph "Range Partitioning" A[Keys A-F] -->|Range: A-F| S0[Shard 0] B[Keys G-M] -->|Range: G-M| S1[Shard 1] C[Keys N-S] -->|Range: N-S| S2[Shard 2] D[Keys T-Z] -->|Range: T-Z| S3[Shard 3] end style S0 fill:#339af0 style S1 fill:#339af0 style S2 fill:#339af0 style S3 fill:#339af0
Pros Cons
βœ“ Range queries efficient (scan single shard) βœ— Hot spots if keys non-uniform (e.g., timestamps)
βœ“ Ordered scans possible βœ— Manual rebalancing when ranges grow unevenly
βœ“ Easy to understand and debug βœ— Sequential writes all go to one shard
βœ“ Partition boundaries can be manually tuned βœ— Requires coordination service (e.g., ZooKeeper) for range map

Hot Spot Example: Timestamp Keys

Partition by date:
2024-01-01 to 2024-06-30 β†’ Shard 0
2024-07-01 to 2024-12-31 β†’ Shard 1
2025-01-01 to 2025-06-30 β†’ Shard 2  ← ALL current writes!
2025-07-01 to 2025-12-31 β†’ Shard 3

Problem: All new events go to Shard 2, while Shards 0-1 are idle.

2.2 Hash Partitioning

Mechanism: Hash the partition key, modulo by number of shards.

Example: shard = hash(user_id) % num_shards

graph TD subgraph "Hash Partitioning" U1[User 12345] -->|hash % 4 = 1| S1[Shard 1] U2[User 67890] -->|hash % 4 = 3| S3[Shard 3] U3[User 11111] -->|hash % 4 = 0| S0[Shard 0] U4[User 99999] -->|hash % 4 = 2| S2[Shard 2] U5[User 55555] -->|hash % 4 = 1| S1 U6[User 44444] -->|hash % 4 = 0| S0 end style S0 fill:#20c997 style S1 fill:#20c997 style S2 fill:#20c997 style S3 fill:#20c997
Pros Cons
βœ“ Uniform distribution (avoids hot spots) βœ— Range queries require scatter-gather (hit all shards)
βœ“ Simple routing logic (no coordination service) βœ— Rebalancing requires rehashing (moves lots of data)
βœ“ No manual tuning of boundaries βœ— Ordered scans impossible
βœ“ Sequential writes distributed evenly βœ— Can't easily add/remove shards (consistent hashing solves this)
Interview Tip: Hash partitioning trades query flexibility for scalability. Use when write throughput > read patterns, or when most queries are point lookups by key.

2.3 Compound/Composite Keys (DynamoDB Model)

Mechanism: Partition by hash of partition key, sort within partition by sort key.

Example: DynamoDB: partition_key = tenant_id (hashed), sort_key = timestamp (sorted)

graph TD subgraph "Compound Key Partitioning" T1[Tenant A
hash→0] --> S0[Shard 0
Sorted by timestamp] T2[Tenant B
hash→1] --> S1[Shard 1
Sorted by timestamp] T3[Tenant C
hash→0] --> S0 T4[Tenant D
hash→2] --> S2[Shard 2
Sorted by timestamp] end S0 --> Q1[Query: Tenant A
Last 7 days
Single shard scan] S1 --> Q2[Query: Tenant B
Last 7 days
Single shard scan]
Aspect Details
Partition key Determines shard (hash-based distribution)
Sort key Determines order within partition (range queries efficient)
Best for Multi-tenant systems, time-series data per entity
Example IoT: device_id (partition) + timestamp (sort)
Query patterns Efficient: single partition + sort key range
Inefficient: cross-partition aggregation

2.4 Directory-Based Partitioning

Mechanism: Maintain a lookup table mapping keys (or key ranges) to shards.

Example: Vitess (MySQL): VSchema maps keyspace ranges to tablets

graph TD C[Client] -->|1. Query key X| D[Directory Service] D -->|2. Lookup: X β†’ Shard 2| C C -->|3. Query Shard 2| S2[Shard 2] subgraph "Directory Table" D --> DT[Key Range β†’ Shard
A-F β†’ 0
G-M β†’ 1
N-S β†’ 2
T-Z β†’ 3] end style D fill:#ffd43b style S2 fill:#339af0
Pros Cons
βœ“ Flexible remapping (change shard assignments without moving data immediately) βœ— Extra hop (latency overhead)
βœ“ Supports dynamic rebalancing βœ— Directory service is SPOF (must be HA)
βœ“ Can use any partitioning logic (range, hash, geo, custom) βœ— Cache invalidation complexity
βœ“ Enables online shard migration βœ— Coordination overhead (ZooKeeper, etcd)

2.5 Geographic Partitioning

Mechanism: Partition by geographic region (country, continent, data center).

Use case: Data locality for latency, compliance (GDPR: EU data in EU).

graph LR subgraph "Geographic Partitioning" US[US Users] --> DC_US[US West DC
Shard US] EU[EU Users] --> DC_EU[EU Central DC
Shard EU] ASIA[Asia Users] --> DC_ASIA[Singapore DC
Shard ASIA] end DC_US --> R_US[Replica US East] DC_EU --> R_EU[Replica EU West] DC_ASIA --> R_ASIA[Replica Tokyo] style DC_US fill:#51cf66 style DC_EU fill:#51cf66 style DC_ASIA fill:#51cf66
Pros Cons
βœ“ Low latency (data close to users) βœ— Cross-region queries expensive
βœ“ Compliance with data residency laws βœ— Uneven data distribution (US has 50% of users)
βœ“ Fault isolation (region outage doesn't affect others) βœ— Complex routing (need geo-IP lookup)
βœ“ Can use region-specific infrastructure/pricing βœ— User migration issues (moving countries)

Strategy Comparison

Strategy Use Case Range Queries Hot Spot Risk Rebalancing Examples
Range Time-series, analytics Efficient High Manual HBase, Bigtable
Hash User data, KV stores Scatter-gather Low Rehash all MongoDB (pre-3.4)
Compound Multi-tenant, IoT Per-partition Medium Split hot partitions DynamoDB, Cassandra
Directory Flexible routing Depends Depends Update mapping Vitess
Geographic Global apps, compliance Per-region High (region skew) Rare Cloud Spanner

3. Hot Spots & Skew

The Celebrity Problem

Scenario: Twitter-like system, hash-partition by user_id. Celebrity with 100M followers posts a tweet.

Problem: All 100M followers' timeline writes go to the same shard (celebrity's shard), overwhelming it.

graph TD subgraph "Without Mitigation" C[Celebrity Tweet] --> H[hash user_id % 4 = 2] H --> S2[Shard 2
100M writes
OVERLOADED] N1[Normal User 1] --> H1[hash % 4 = 0] H1 --> S0[Shard 0
100 writes] N2[Normal User 2] --> H2[hash % 4 = 1] H2 --> S1[Shard 1
100 writes] end style S2 fill:#ff6b6b,color:#1a1a2e style S0 fill:#51cf66 style S1 fill:#51cf66

3.1 Key Salting / Spreading

Mechanism: For hot keys, append a random suffix to distribute across multiple shards.

Original key: celebrity_id = 12345
Salted keys:  celebrity_id_0, celebrity_id_1, ..., celebrity_id_9

Read:  Fan-out to all 10 salted keys, merge results
Write: Randomly pick one salt (or write to all for updates)

Worked Example: Celebrity Timeline

Setup:
- Celebrity ID: 99999
- Followers: 100M
- Shards: 4 (hash-partitioned)
- Without salting: hash(99999) % 4 = 3 β†’ ALL writes to Shard 3

With salting (salt = 10):
- Store 10 copies: timeline_99999_0, timeline_99999_1, ..., timeline_99999_9
- Write: Pick random salt β†’ write to 1 of 10 keys β†’ distributed across shards
- Read: Fan-out to all 10 keys, merge and sort by timestamp

Result:
- Writes: 100M / 10 = 10M per salted key β†’ 2.5M per shard (balanced)
- Reads: 10x fan-out (acceptable for 100M followers - they can afford it)

3.2 Splitting Hot Partitions

Mechanism: Detect hot partitions (by monitoring metrics), split them dynamically.

Example: Range partition [A-M] is hot β†’ split into [A-F], [G-M]

graph TD subgraph "Before Split" P1[Partition A-M
80% of traffic] P2[Partition N-Z
20% of traffic] end P1 -->|Detect hot partition| SPLIT[Auto-Split] subgraph "After Split" SPLIT --> P1A[Partition A-F
40% of traffic] SPLIT --> P1B[Partition G-M
40% of traffic] P2 --> P2A[Partition N-Z
20% of traffic] end style P1 fill:#ff6b6b,color:#1a1a2e style P1A fill:#51cf66 style P1B fill:#51cf66
System Hot Partition Handling
DynamoDB Adaptive capacity: auto-scale hot partitions, isolate hot keys to dedicated throughput
MongoDB Split chunks when they exceed 64MB or high write rate detected
HBase Auto-split regions at configurable size/load thresholds
Cassandra Manual: increase replication factor for hot keys, client-side salting

3.3 Random Prefix Strategy

Mechanism: Prepend timestamp keys with random prefix to avoid sequential write hot spot.

Original key: 2024-05-12T10:30:00_event123
With prefix:  7_2024-05-12T10:30:00_event123  (prefix 0-9)

All writes distributed, but range scans now hit all partitions.

Scenario: IoT Event Stream

Problem:
- 1M IoT devices sending events with timestamp keys
- All current events have same timestamp prefix (2024-05-12T10:30:*)
- Range-partitioned by timestamp β†’ all writes to one partition

Solution: Random 2-digit prefix
Key format: [00-99]_timestamp_device_id

Writes: Uniformly distributed across ~100 buckets
Reads (last 1 hour): Fan-out to all 100 buckets, merge-sort

Trade-off:
- Write throughput: 100x improvement
- Read latency: 100x fan-out (but parallelizable)

Detecting Hot Spots

Metric Threshold Action
CPU utilization One shard >80%, others <40% Investigate key distribution, consider splitting
Request rate One shard 5x avg Hot key likely, enable salting
Throttling events DynamoDB: ProvisionedThroughputExceeded on one partition Enable adaptive capacity, increase partition count
Latency P99 One shard P99 >500ms, others <50ms Hot partition overloaded, split immediately
Interview Tip: Always mention hot spot mitigation when designing hash-partitioned systems. Interviewer will ask "What if one celebrity has 100M followers?"

4. Secondary Indexes in Partitioned Databases

Problem: Primary key partitioning (e.g., user_id) is efficient for lookups by user_id. But what if you need to query by email or country?

Solution: Secondary indexes. But in a partitioned DB, where do you store them?

4.1 Local Secondary Indexes (Scatter-Gather on Reads)

Mechanism: Each shard maintains its own index for documents in that shard.

Query path: Query all shards (scatter), merge results (gather).

graph TD subgraph "Local Indexes (partition-by-document)" S0[Shard 0
Users A-M
Index: country] --> D0[A-M docs indexed locally] S1[Shard 1
Users N-Z
Index: country] --> D1[N-Z docs indexed locally] end Q[Query: country=USA] -->|Scatter| S0 Q -->|Scatter| S1 S0 -->|US users A-M| M[Merge & Sort] S1 -->|US users N-Z| M M --> R[Final Result] style Q fill:#ffd43b style M fill:#ffd43b
Pros Cons
βœ“ Fast writes (index update local to document shard) βœ— Slow reads (must query all shards)
βœ“ Strong consistency (index and document co-located) βœ— Tail latency amplification (slowest shard blocks response)
βœ“ No cross-shard coordination on writes βœ— Network overhead (N shard queries)

DynamoDB Local Secondary Index

Table: Users (partition key: user_id, sort key: timestamp)
LSI: GSI on email (sort key: email)

- Email index stored within each partition
- Query by email: DynamoDB internally scans all partitions
- Limitation: LSI shares partition key with table (can't change partition scheme)

4.2 Global Secondary Indexes (Scatter on Writes, Fast Reads)

Mechanism: Index is independently partitioned (different partition key from main table).

Example: Main table partitioned by user_id, index partitioned by country.

graph TD subgraph "Global Index (partition-by-term)" M[Main Table
Partitioned by user_id] I0[Index Shard 0
Countries A-M] I1[Index Shard 1
Countries N-Z] end W[Write: User in USA] -->|1. Write user| M W -->|2. Update index| I1 Q[Query: country=USA] -->|Single shard lookup| I1 I1 -->|User IDs in USA| R[Result] style W fill:#ff6b6b,color:#1a1a2e style Q fill:#51cf66
Pros Cons
βœ“ Fast reads (query one index shard) βœ— Slow writes (may need to update index on different shard)
βœ“ No scatter-gather for index queries βœ— Eventual consistency (index update async)
βœ“ Index partitioned optimally for query pattern βœ— Cross-shard writes (coordination overhead)

DynamoDB Global Secondary Index

Table: Users (partition key: user_id)
GSI: EmailIndex (partition key: email)

- Email index is separate table, partitioned by email
- Query by email: hits one partition (fast)
- Write user: updates main table + async update GSI (eventual consistency)
- GSI has its own provisioned throughput

Partition-by-Document vs Partition-by-Term

Aspect Partition-by-Document (Local Index) Partition-by-Term (Global Index)
Index location Co-located with document shard Separate partition scheme
Write path Update doc + local index (single shard) Update doc shard + index shard (two shards)
Read path Query all shards, merge Query one index shard
Consistency Strong (atomic with doc) Eventual (async update)
Use case Write-heavy, rare index queries Read-heavy, frequent index queries
Example MongoDB (default), DynamoDB LSI Elasticsearch, DynamoDB GSI

Scatter-Gather Pattern

sequenceDiagram participant C as Client participant R as Router participant S0 as Shard 0 participant S1 as Shard 1 participant S2 as Shard 2 participant S3 as Shard 3 C->>R: Query: country=USA par Scatter R->>S0: Query local index R->>S1: Query local index R->>S2: Query local index R->>S3: Query local index end par Gather S0-->>R: 25 results S1-->>R: 30 results S2-->>R: 20 results S3-->>R: 25 results end R->>R: Merge, sort, paginate R-->>C: Top 50 results
Interview Tip: When asked "How do you query by non-partition-key fields?", discuss trade-offs:
  • Local index: write-optimized, eventual consistency not required
  • Global index: read-optimized, can tolerate async index updates
  • Denormalization: duplicate data to avoid indexes (e.g., store user's country in every order record)

5. Consistent Hashing

The Rebalancing Problem with Naive Hashing

Naive hash: shard = hash(key) % N

Problem: Adding/removing a shard changes N β†’ rehash ALL keys

Example: 4 shards β†’ 5 shards
  Old: hash(key) % 4
  New: hash(key) % 5
  β†’ 80% of keys move to different shards!

5.1 Hash Ring Concept

Mechanism:

  • Hash space is a ring: 0 to 2^32-1, wraps around
  • Nodes placed at random points on ring (hash of node ID)
  • Keys placed on ring (hash of key)
  • Key assigned to first node clockwise from its position
graph TD subgraph "Consistent Hash Ring" direction LR A[Node A
hash=100] B[Node B
hash=600] C[Node C
hash=1100] D[Node D
hash=1600] K1[Key X
hash=300] -->|Clockwise| B K2[Key Y
hash=800] -->|Clockwise| C K3[Key Z
hash=1300] -->|Clockwise| D K4[Key W
hash=1800] -->|Clockwise→wrap| A end style A fill:#339af0 style B fill:#339af0 style C fill:#339af0 style D fill:#339af0

Adding/Removing Nodes

Adding Node E (hash=900)

Before:
Node B (600) owns keys 101-600
Node C (1100) owns keys 601-1100

After adding Node E (900):
Node B (600) owns keys 101-600  (unchanged)
Node E (900) owns keys 601-900  (moved from C)
Node C (1100) owns keys 901-1100

Result: Only keys in range 601-900 move (from C to E).
β†’ Minimal data movement (~1/N of total data for N nodes)
sequenceDiagram participant R as Ring participant E as New Node E participant C as Node C R->>R: Add E at position 900 R->>C: Transfer keys 601-900 C->>E: Move ~1/N of data R->>R: Update routing table Note over R: All other nodes unaffected

5.2 Virtual Nodes (VNodes)

Problem: With few nodes, hash distribution can be uneven (one node owns 40%, another 10%).

Solution: Each physical node represented by multiple virtual nodes on the ring.

graph TD subgraph "Without VNodes" P1[Physical Node A
40% of ring] P2[Physical Node B
10% of ring] P3[Physical Node C
50% of ring] end subgraph "With VNodes 256 per node" V1[Node A:
V1, V2, ..., V256
~33% of ring] V2[Node B:
V1, V2, ..., V256
~33% of ring] V3[Node C:
V1, V2, ..., V256
~33% of ring] end style P2 fill:#ff6b6b,color:#1a1a2e style V1 fill:#51cf66 style V2 fill:#51cf66 style V3 fill:#51cf66
System VNodes per Node Rationale
Cassandra 256 (default) Balance load distribution vs metadata overhead
Riak 64 (default) Lower overhead for smaller clusters
DynamoDB ~1000s (internal) Fine-grained rebalancing, adaptive capacity

5.3 Jump Consistent Hash (Alternative)

Problem: Hash ring requires O(N) metadata (node positions), O(log N) lookup.

Jump Hash: Stateless algorithm, O(1) space, O(log N) compute.

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;
}
Aspect Hash Ring Jump Hash
Metadata O(N) node positions O(1) - just num_buckets
Lookup time O(log N) binary search O(log N) iterations
Add node Minimal movement, any position Minimal movement, append only
Remove node Any node Must remove last node (or rehash)
Use case General sharding (Cassandra, DynamoDB) Fixed-order scaling (Google load balancers)
Interview Tip: Mention consistent hashing when discussing:
  • Caching layers (Memcached, Redis cluster)
  • Distributed databases (Cassandra, Riak)
  • Load balancing (L7 sticky sessions)
  • CDN routing (Akamai)

Consistent Hashing in Real Systems

System Implementation Details
DynamoDB Consistent hashing with virtual nodes. Partition split/merge automatic. Adaptive capacity isolates hot keys.
Cassandra Token ring (MD5 hash). 256 vnodes/node default. Vnodes improve load balance when adding nodes.
Riak SHA-1 hash ring. 64 vnodes/node. Quorum reads/writes (N=3, R=2, W=2).
Memcached Client-side consistent hashing (libketama). Server-side unaware of ring.

6. Rebalancing Strategies

Why rebalance?

  • Data growth: partitions exceed size limits
  • Query throughput: one partition overloaded
  • Cluster expansion: added new nodes, need to spread load
  • Cluster shrink: removed nodes, need to redistribute data

6.1 Fixed Number of Partitions

Mechanism: Create many more partitions than nodes upfront. Assign multiple partitions per node. When adding nodes, move whole partitions.

Example: 1000 partitions, 10 nodes β†’ 100 partitions/node. Add 5 nodes β†’ rebalance to 67 partitions/node.

graph TD subgraph "Fixed Partitions: Before Adding Nodes" N1[Node 1
P1-P100] N2[Node 2
P101-P200] N3[Node 3
P201-P300] end subgraph "After Adding Node 4" N1A[Node 1
P1-P75] N2A[Node 2
P101-P175] N3A[Node 3
P201-P275] N4[Node 4
P76-P100
P176-P200
P276-P300] end N1 --> N1A N2 --> N2A N3 --> N3A N1 -.Move P76-P100.-> N4 N2 -.Move P176-P200.-> N4 N3 -.Move P276-P300.-> N4
Pros Cons
βœ“ Simple to reason about (partitions are immutable units) βœ— Must choose partition count upfront (hard to change later)
βœ“ Rebalancing is just partition reassignment (no data splitting) βœ— Too few partitions: can't scale beyond initial count
βœ“ Easy to replicate (each partition independently replicated) βœ— Too many partitions: metadata overhead, GC pauses

Elasticsearch: Fixed Shards

Index created with 5 primary shards (immutable)

Day 1: 1 node β†’ all 5 shards on Node 1
Day 30: Add Node 2 β†’ rebalance to 3 shards on N1, 2 on N2
Day 60: Add Node 3 β†’ rebalance to 2-2-1 distribution

Cannot add more than 5 nodes (only 5 shards to distribute).
To scale beyond 5 nodes: reindex with more shards (expensive).

6.2 Dynamic Partitioning

Mechanism: Start with small number of partitions. Split partitions when they grow too large. Merge when too small.

Example: HBase starts with 1 region per table. Auto-split at 10GB (configurable).

graph TD subgraph "Dynamic Partitioning" P1[Partition A-Z
15GB] -->|Exceeds 10GB threshold| SPLIT[Auto-Split] SPLIT --> P1A[Partition A-M
7GB] SPLIT --> P1B[Partition N-Z
8GB] P1A -->|Growth| P1A_NEW[Partition A-M
12GB] P1A_NEW -->|Exceeds 10GB| SPLIT2[Split Again] SPLIT2 --> P1AA[A-F: 6GB] SPLIT2 --> P1AB[G-M: 6GB] end style P1 fill:#ff6b6b,color:#1a1a2e style P1A fill:#51cf66 style P1B fill:#51cf66
Pros Cons
βœ“ Adapts to data growth (no upfront sizing) βœ— Uneven distribution at start (few large partitions)
βœ“ Can scale arbitrarily (split as needed) βœ— Split/merge overhead (data movement)
βœ“ Optimal partition size maintained over time βœ— Cold start problem (new table has 1 partition until first split)

MongoDB: Chunk-Based Splitting

Initial: 1 chunk (entire key range)

Insert 100K docs β†’ chunk exceeds 64MB β†’ split into 2 chunks
Continue growth β†’ each chunk splits independently

Balancer: Background process moves chunks between shards to balance
- Runs periodically (every 10 min)
- Moves one chunk at a time (to minimize impact)
- Goal: equalize chunk count across shards

6.3 Proportional Partitioning

Mechanism: Number of partitions proportional to node count (e.g., 10 partitions per node).

Example: 5 nodes β†’ 50 partitions. Add 5 nodes β†’ split to 100 partitions, rebalance.

Pros Cons
βœ“ Partition size scales with cluster size βœ— More complex: must split partitions when adding nodes
βœ“ Avoids too many/too few partitions βœ— Rebalancing triggers partition splits (more data movement)

Strategy Comparison

Strategy Partition Count Rebalance Trigger Cold Start Examples
Fixed Set at creation Add/remove nodes Good (many partitions) Riak, ES, Couchbase
Dynamic Grows with data Size threshold Poor (1 partition initially) HBase, MongoDB
Proportional N Γ— nodes Add/remove nodes + split Medium Some cloud DBs

Rebalancing Best Practices

1. Incremental Rebalancing

Move one partition at a time, not all at once. Prevents overwhelming the network/cluster.

2. Rate Limiting

Throttle data movement (e.g., max 100MB/s per node). Avoid impacting prod traffic.

3. Prioritization

Move small partitions first (faster, lower risk). Save large partitions for off-peak hours.

4. Snapshot-Based Migration

Take snapshot, copy to new node, replay incremental writes. Minimizes downtime.

5. Monitoring

Track progress, partition counts, data distribution. Rollback mechanism if issues.

Interview Tip: When asked "How do you add nodes to your sharded DB?", describe:
  • Rebalancing strategy (fixed, dynamic, proportional)
  • Data movement mechanics (incremental, rate-limited)
  • Consistency during migration (read-repair, dual-write)
  • Monitoring (partition distribution, query performance)

7. Cross-Shard Queries

Problem: Query needs data from multiple shards (e.g., "Get all orders for product X" when partitioned by customer_id).

Solution: Scatter-gather pattern (query all shards, merge results).

7.1 Scatter-Gather Pattern

sequenceDiagram participant C as Client participant R as Router/Coordinator participant S0 as Shard 0 participant S1 as Shard 1 participant S2 as Shard 2 participant S3 as Shard 3 C->>R: SELECT * FROM orders
WHERE product_id = 123
LIMIT 100 Note over R: Parse query, determine
requires all shards par Scatter to all shards R->>S0: SELECT ... LIMIT 100 R->>S1: SELECT ... LIMIT 100 R->>S2: SELECT ... LIMIT 100 R->>S3: SELECT ... LIMIT 100 end par Gather results S0-->>R: 40 rows S1-->>R: 60 rows S2-->>R: 30 rows S3-->>R: 50 rows end Note over R: Merge 180 rows,
sort, apply LIMIT 100 R-->>C: 100 rows

Fan-Out Cost

Scenario: E-Commerce Orders

Setup:
- 100 shards (partitioned by customer_id)
- Query: "All orders for product X in last 7 days"
- Product X: 10K orders across 5K customers

Without optimization:
- Fan-out: 100 shards
- Each shard scans its orders for product X
- Network cost: 100 queries + 100 responses
- Latency: max(shard latencies) = tail latency problem

With secondary index on product_id (global):
- Query product_id index: returns 10K order IDs + customer IDs
- Group by customer_id β†’ 5K distinct customers across 100 shards
- Still need to fetch from ~100 shards (customers distributed)

With denormalization:
- Maintain product_orders table (partitioned by product_id)
- Query: single-shard lookup
- Trade-off: duplicate data, write amplification
Metric Single Shard Scatter-Gather (10 shards) Scatter-Gather (100 shards)
Network calls 1 10 100
Latency (P50) 10ms 15ms (10ms + 5ms merge) 25ms (10ms + 15ms merge)
Latency (P99) 50ms 100ms (tail latency) 500ms (one slow shard)
Throughput 10K QPS/shard 1K QPS (10x load per shard) 100 QPS (100x load per shard)

7.2 Avoidance Strategies

Strategy 1: Denormalization

Approach: Duplicate data to support different query patterns without cross-shard access.

Original:
- orders table (partition by customer_id)

Denormalized:
- orders_by_customer (partition by customer_id)
- orders_by_product (partition by product_id)
- orders_by_date (partition by date)

Trade-off: 3x storage, write amplification, eventual consistency

Strategy 2: Materialized Views

Approach: Pre-compute aggregations across shards, store in separate table.

Example: Daily sales by product
- Background job: scatter-gather query all shards
- Compute SUM(amount) per product per day
- Store in product_daily_sales (single shard or small table)

Query: Fast lookup in materialized view (no cross-shard query)
Freshness: Updated hourly/daily (not real-time)

Strategy 3: Application-Level Joins

Approach: Fetch data in multiple round trips, join in application code.

Query: "Get customer details + recent orders"

Step 1: Fetch customer from customers table (partition by customer_id)
Step 2: Fetch orders for that customer from orders table (same shard)
Step 3: Join in application code

Cost: 2 sequential queries (OK if both hit same shard)

Cross-Shard JOIN Cost

graph TD subgraph "Cross-Shard JOIN Example" Q[Query: SELECT * FROM orders
JOIN products ON orders.product_id = products.id
WHERE customer_id = 123] Q -->|1. Fetch orders| OS[Orders Shard
customer 123: 50 orders] OS -->|2. Extract product IDs| PID[Product IDs:
10 distinct products] PID -->|3. Fan-out to product shards| PS0[Product Shard 0] PID --> PS1[Product Shard 1] PID --> PS2[Product Shard 2] PS0 -->|4. Merge| M[Join in coordinator] PS1 --> M PS2 --> M M --> R[Final result: 50 rows] end style Q fill:#ffd43b style M fill:#ff6b6b,color:#1a1a2e
Interview Tip: When designing sharded systems, ask:
  • "What are the primary query patterns?" (optimize partitioning for these)
  • "Can we denormalize to avoid cross-shard queries?" (usually yes)
  • "Is real-time consistency required?" (if no, use materialized views)

8. Cross-Shard Transactions

Problem: Transaction spans multiple shards (e.g., transfer $100 from account on Shard 1 to account on Shard 2).

Challenge: ACID guarantees are hard across shards (no single coordinator).

8.1 Two-Phase Commit (2PC)

Mechanism: Coordinator asks all shards to prepare (Phase 1), then commit (Phase 2) if all agree.

sequenceDiagram participant C as Client participant Coord as Coordinator participant S1 as Shard 1 participant S2 as Shard 2 C->>Coord: BEGIN TX:
Transfer $100
from Acct A to B Note over Coord: Phase 1: PREPARE Coord->>S1: PREPARE: Deduct $100 from A Coord->>S2: PREPARE: Add $100 to B S1-->>Coord: PREPARED (locked A) S2-->>Coord: PREPARED (locked B) Note over Coord: All participants ready.
Write decision to durable log. Note over Coord: Phase 2: COMMIT Coord->>S1: COMMIT Coord->>S2: COMMIT S1-->>Coord: COMMITTED (unlock A) S2-->>Coord: COMMITTED (unlock B) Coord-->>C: TX SUCCESS
Pros Cons
βœ“ Strong consistency (ACID across shards) βœ— Blocking: participants hold locks during prepare phase
βœ“ Well-understood protocol βœ— Coordinator failure: shards blocked forever (need timeout + recovery)
βœ“ Supported by many DBs (MySQL XA, PostgreSQL) βœ— High latency (2 network round trips)
βœ— Throughput bottleneck (coordinator is hotspot)

2PC Failure Scenario

Scenario: Coordinator crashes after sending PREPARE, before COMMIT.

Shard states:
- Shard 1: PREPARED (holding lock on account A)
- Shard 2: PREPARED (holding lock on account B)
- Coordinator: CRASHED (decision unknown)

Problem: Shards don't know whether to commit or abort. Can't release locks (violate atomicity).

Solution:
1. Coordinator writes COMMIT decision to durable log before sending
2. On recovery, read log β†’ resend COMMIT/ABORT to shards
3. Shards timeout after N seconds, query coordinator status

8.2 Saga Pattern (Compensating Transactions)

Mechanism: Break transaction into independent local transactions. If one fails, run compensating transactions to undo previous steps.

Trade-off: Eventual consistency (not ACID), but no blocking locks.

sequenceDiagram participant C as Client participant Coord as Saga Coordinator participant S1 as Shard 1 participant S2 as Shard 2 C->>Coord: Transfer $100 A→B Coord->>S1: T1: Deduct $100 from A S1-->>Coord: SUCCESS (committed) Coord->>S2: T2: Add $100 to B S2-->>Coord: FAILURE (account B frozen) Note over Coord: T2 failed. Run compensating TX. Coord->>S1: C1: Add $100 back to A
(compensate for T1) S1-->>Coord: COMPENSATED Coord-->>C: TX ABORTED (compensated)
Pros Cons
βœ“ Non-blocking (no distributed locks) βœ— Not ACID (intermediate states visible)
βœ“ High availability (no coordinator blocking) βœ— Compensating logic complexity (must be idempotent)
βœ“ Better performance (local transactions fast) βœ— Eventual consistency only
βœ“ Scales well (no coordinator hotspot) βœ— Rollback may fail (need dead-letter queue + manual intervention)

Saga Example: E-Commerce Order

Steps:
T1: Reserve inventory (inventory shard)
T2: Charge payment (payment shard)
T3: Create order (orders shard)
T4: Send notification (notifications shard)

Failure at T3 (order creation failed):
C2: Refund payment
C1: Release inventory reservation

Eventual state: All or nothing (but intermediate states exist)

8.3 Calvin Protocol (Deterministic Ordering)

Mechanism: Pre-order all transactions deterministically before execution. All shards execute in same order (no coordination needed).

Example: FaunaDB, derived from Calvin research.

graph TD subgraph "Calvin Protocol" T1[TX 1] --> S[Sequencer] T2[TX 2] --> S T3[TX 3] --> S S -->|Global order:
T1, T2, T3| A[All shards execute
in same order] A --> S1[Shard 1:
Execute T1,T2,T3
in order] A --> S2[Shard 2:
Execute T1,T2,T3
in order] A --> S3[Shard 3:
Execute T1,T2,T3
in order] end style S fill:#ffd43b style S1 fill:#51cf66 style S2 fill:#51cf66 style S3 fill:#51cf66
Pros Cons
βœ“ Serializable isolation (deterministic execution) βœ— Requires pre-declaration of read/write sets
βœ“ No distributed locking (all shards agree on order) βœ— False conflicts (may abort TXs that could have committed)
βœ“ High throughput (pipelined execution) βœ— Latency overhead (sequencing delay)

8.4 Google Spanner's Approach

Key innovation: TrueTime API (GPS + atomic clocks) provides globally synchronized time with bounded uncertainty.

Result: Serializable distributed transactions without blocking (external consistency).

Spanner Transaction

Mechanism:
1. Acquire locks across shards (Paxos groups)
2. Assign commit timestamp t = TT.now().latest
3. Wait until TT.after(t) is true (uncertainty interval passes)
4. Release locks

Why wait? Ensures commit timestamp is in the past for all nodes (no clock skew issues).

Trade-off:
- Pro: Serializable, externally consistent transactions
- Con: 2Γ— RTT latency for cross-region TXs (waiting for TrueTime uncertainty)

Approach Comparison

Approach Consistency Latency Availability Use Case
2PC ACID High (blocking) Low (coordinator SPOF) Legacy systems, rare cross-shard TXs
Saga Eventual Low High Microservices, e-commerce
Calvin Serializable Medium Medium FaunaDB, research systems
Spanner Serializable High (TrueTime wait) High (Paxos) Google Cloud Spanner, global apps
Interview Tip: For cross-shard transactions, ask:
  • "Do we need ACID?" β†’ If yes, 2PC or Spanner. If no, Saga.
  • "How frequent are cross-shard TXs?" β†’ If rare, 2PC OK. If common, avoid or use Saga.
  • "Can we partition to avoid cross-shard TXs?" β†’ Best solution: co-locate related data.

9. Shard Key Selection

Most critical decision: Shard key determines data distribution, query patterns, scalability.

Wrong shard key: Hot spots, cross-shard queries, expensive resharding.

Selection Criteria

Criterion Why It Matters Example
High cardinality More unique values β†’ better distribution Good: user_id (millions)
Bad: country (200)
Uniform distribution Avoid hot spots (equal load per shard) Good: UUID
Bad: timestamp (writes all go to latest)
Matches access pattern Most queries should hit single shard Good: tenant_id for SaaS
Bad: random_id for per-tenant queries
Stable over time Avoid resharding (expensive) Good: user_id
Bad: age (changes, requires updates)
Growth predictable Plan capacity, avoid uneven growth Good: hash(user_id)
Bad: company_size (some companies grow 100x)

Decision Framework

graph TD START[Choose Shard Key] --> Q1{Primary query
pattern?} Q1 -->|By user| UK[User ID] Q1 -->|By tenant| TK[Tenant ID] Q1 -->|By time range| TM[Timestamp] Q1 -->|No dominant pattern| HASH[Hash of primary key] UK --> Q2{Hot users
possible?} Q2 -->|Yes| SALT[User ID + salt] Q2 -->|No| UK_FINAL[Shard by user_id] TK --> Q3{Tenants vary
in size?} Q3 -->|Yes| TK_SPLIT[Shard by tenant_id
+ auto-split large tenants] Q3 -->|No| TK_FINAL[Shard by tenant_id] TM --> Q4{Writes concentrated
in recent time?} Q4 -->|Yes| TM_PREFIX[Random prefix + timestamp] Q4 -->|No| TM_FINAL[Shard by timestamp] HASH --> HASH_FINAL[Shard by hash primary key] style START fill:#ffd43b style UK_FINAL fill:#51cf66 style TK_FINAL fill:#51cf66 style TM_FINAL fill:#51cf66 style HASH_FINAL fill:#51cf66

Bad Shard Key Examples

Example 1: Low Cardinality (Country)

Shard key: country

Problem:
- Only ~200 unique values
- US has 50% of users β†’ one shard overloaded
- Can't scale beyond 200 shards (one per country)

Better: hash(user_id) for uniform distribution, denormalize country for filtering

Example 2: Monotonic Key (Auto-Increment ID)

Shard key: order_id (auto-increment)

Problem:
- All new orders go to highest shard (e.g., order_id 1M-2M on Shard 10)
- Shard 10 gets all writes, Shards 0-9 idle
- Hot spot, no write parallelism

Better: hash(order_id) for uniform writes

Example 3: Mutable Key (User Status)

Shard key: user_status (active, inactive, suspended)

Problem:
- User changes status β†’ must move to different shard (expensive)
- Active users >> inactive β†’ skewed distribution
- Resharding on every status change

Better: Immutable key (user_id), filter by status in application

Good Shard Key Examples

Use Case Shard Key Rationale
Social media user_id High cardinality, stable, most queries per-user
Multi-tenant SaaS tenant_id Isolation, per-tenant queries, predictable growth
IoT time-series device_id + timestamp Compound: distribute by device, sort by time
E-commerce orders hash(customer_id) Uniform distribution, per-customer queries
Global user data region + hash(user_id) Geographic partitioning + uniform within region

Testing Shard Key Quality

Metrics to Monitor

  • Data distribution: Standard deviation of data size per shard (goal: <10% variance)
  • Request distribution: CV (coefficient of variation) of QPS per shard (goal: <0.2)
  • Hot key frequency: % of requests hitting top 1% of keys (goal: <5%)
  • Cross-shard query rate: % of queries requiring scatter-gather (goal: <10%)

Quality Assessment Example

Shard key candidate: hash(user_id)

Test with production traffic sample (1M requests):

Data distribution:
- Shard 0: 10.2M rows (10.2%)
- Shard 1: 9.8M rows (9.8%)
- ...
- Shard 9: 10.1M rows (10.1%)
- Std dev: 0.4% βœ“ (excellent)

Request distribution:
- Shard 0: 102K QPS
- Shard 1: 98K QPS
- ...
- Shard 9: 101K QPS
- CV: 0.03 βœ“ (excellent)

Hot keys:
- Top 1% of users (celebs): 8% of requests
- With salting (10x): 0.8% per salted key βœ“ (acceptable)

Cross-shard queries:
- 3% of queries (admin reports only) βœ“ (acceptable)

Decision: Proceed with hash(user_id), enable hot key salting for top 0.1% users
Interview Tip: When asked to choose a shard key:
  1. Identify primary query pattern (80% of queries)
  2. Check cardinality (millions of unique values)
  3. Assess distribution (uniform or skewed?)
  4. Consider hot spots (celebrities, popular products)
  5. Validate with metrics (test with sample data)

10. Resharding & Migration

When to reshard:

  • Original shard count insufficient (outgrew initial estimate)
  • Bad shard key chosen (hot spots, cross-shard queries)
  • Data skew developed over time (uneven growth)
  • Major workload change (OLTP β†’ analytics)

10.1 Online Migration Strategies

Strategy 1: Double-Write

Mechanism: Write to both old and new shards during migration. Gradually shift reads to new shards.

sequenceDiagram participant A as Application participant O as Old Shards participant N as New Shards Note over A,N: Phase 1: Backfill old data A->>N: Async copy old data Note over A,N: Phase 2: Double-write new data A->>O: Write A->>N: Write A->>O: Read (primary) Note over A,N: Phase 3: Shift reads to new A->>O: Write A->>N: Write A->>N: Read (primary) A->>O: Read (fallback if not found) Note over A,N: Phase 4: Decommission old shards A->>N: Write only A->>N: Read only
Phase Duration Risk Rollback
Backfill Days-weeks Low (read-only) Easy (abort copy)
Double-write Days Medium (write amplification) Medium (stop new writes)
Read shift Hours High (if data inconsistent) Easy (flip flag)
Decommission Days Low (reads validated) Hard (data deleted)

Strategy 2: Shadow Traffic

Mechanism: Replay production traffic to new shards (reads only), validate correctness before cutover.

1. Mirror 1% of reads to new shards (discard results)
2. Compare results: old vs new (alert on mismatch)
3. Gradually increase to 100% shadow traffic
4. Once validated, flip cutover flag (reads go to new shards)
5. Enable writes to new shards, stop old shards

10.2 System-Specific Migration Tools

Vitess (MySQL Sharding)

Vitess: Horizontal sharding layer for MySQL. Handles resharding transparently.

Resharding workflow:
1. SplitClone: Copy data from source shard to destination shards
2. Filtered replication: Stream new writes from source to destinations
3. Cutover: Switch traffic from source to destinations (atomic flag flip)
4. Drop source shard after validation

Example: 1 shard β†’ 2 shards
- Original: shard0 (all key ranges)
- Target: shard0 (keys 0x00-0x7F), shard1 (keys 0x80-0xFF)
- Vitess handles routing during migration (app unaware)

MongoDB Balancer

MongoDB: Automatic balancing via chunk migration.

How it works:
1. Config server tracks chunk distribution
2. Balancer runs periodically (every 10 min by default)
3. Detects imbalance: one shard has 8+ more chunks than another
4. Moves one chunk at a time (to minimize impact)
5. Migration steps:
   - Clone chunk to destination shard
   - Apply incremental changes (oplog replay)
   - Atomic cutover (update config server routing)
   - Delete chunk from source shard

Manual resharding: sh.splitAt(), sh.moveChunk() commands

pg_partman (PostgreSQL)

pg_partman: Partition management extension for PostgreSQL.

Features:
- Automatic creation/dropping of time-based/serial-based partitions
- Retention policies (auto-drop old partitions)
- Reindexing, vacuum automation

Resharding approach:
1. Create new partition scheme (e.g., monthly β†’ daily partitions)
2. Backfill data into new partitions (parallel copy)
3. Update partition routing (parent table points to new partitions)
4. Drop old partitions after validation

Not true sharding: Single PostgreSQL instance (vertical scaling limit)

10.3 Backfill Strategies

Strategy Mechanism Pros Cons
Bulk copy Snapshot old shards, copy in batches Simple, fast Stale data (need change capture)
Replication Setup replica of old shards as new shards Real-time sync Requires replication support (MySQL binlog, Postgres WAL)
Change data capture (CDC) Stream changes from old to new (Kafka, Debezium) Near real-time, decoupled Complex setup, eventual consistency
Dual-write at app layer Application writes to both old and new Full control, no DB-level changes Code changes, write amplification

10.4 Migration Checklist

Pre-Migration

  • βœ“ Validate new shard key (run distribution analysis on sample)
  • βœ“ Provision new shards (capacity planning: 50% headroom)
  • βœ“ Setup monitoring (data sync lag, error rate, query latency)
  • βœ“ Test rollback procedure (dry-run on staging)
  • βœ“ Communicate to stakeholders (expected timeline, risk)

During Migration

  • βœ“ Start backfill (low priority, rate-limited)
  • βœ“ Monitor sync lag (alert if >1 hour)
  • βœ“ Enable double-write (validate write amplification acceptable)
  • βœ“ Shadow traffic (validate read results match)
  • βœ“ Gradual cutover (1% β†’ 10% β†’ 50% β†’ 100%)
  • βœ“ Monitor error rate (rollback if >0.1% increase)

Post-Migration

  • βœ“ Validate data completeness (row counts, checksums)
  • βœ“ Performance comparison (query latency P50/P99)
  • βœ“ Monitor for 7 days (no alerts β†’ proceed)
  • βœ“ Decommission old shards (after backup to cold storage)
  • βœ“ Document lessons learned (post-mortem)

Real-World Migration: Instagram (2014)

Background:
- 300M users, 30B photos
- Partitioned by photo_id (auto-increment) β†’ hot spot on latest shard
- Needed to reshard to hash(photo_id) for uniform distribution

Migration approach:
1. Backfill: Bulk copy to new shards (6 weeks, rate-limited to 50MB/s per shard)
2. Change capture: Replicate new writes via binlog (Kafka)
3. Validation: Compare row counts, checksums, spot-check photos
4. Double-write: Application writes to both old and new for 1 week
5. Gradual cutover: 1% β†’ 10% β†’ 50% β†’ 100% over 3 days
6. Decommission: Drop old shards after 2 weeks

Result:
- Zero downtime, <0.01% error rate spike
- Write throughput +40%, P99 latency -60%
- Lessons: Rate-limiting critical, extensive validation saved rollback
Interview Tip: When asked about resharding:
  • Emphasize zero downtime (backfill, double-write, gradual cutover)
  • Mention rollback plan (how to revert if issues)
  • Discuss validation (how to ensure data integrity)
  • Note monitoring (what metrics to track)

11. Real System Examples

11.1 DynamoDB Partitioning

Architecture

  • Partition key: Determines partition (consistent hashing)
  • Sort key (optional): Defines order within partition
  • Partition size limit: 10GB per partition
  • Throughput: 3000 RCUs, 1000 WCUs per partition
graph TD subgraph "DynamoDB Partitioning" TABLE[Table: Users
Partition key: user_id
Sort key: timestamp] TABLE -->|hash user_id| P0[Partition 0
Users A-F
10GB, 3000 RCU] TABLE --> P1[Partition 1
Users G-M
8GB, 3000 RCU] TABLE --> P2[Partition 2
Users N-S
9GB, 3000 RCU] TABLE --> P3[Partition 3
Users T-Z
7GB, 3000 RCU] P0 --> R0[Replica 1] P0 --> R01[Replica 2] P1 --> R1[Replica 1] P1 --> R11[Replica 2] end style TABLE fill:#ffd43b style P0 fill:#339af0 style P1 fill:#339af0 style P2 fill:#339af0 style P3 fill:#339af0

Auto-Scaling & Adaptive Capacity

Partition split:
- Trigger: Partition exceeds 10GB
- Split: Divide key range in half, create 2 new partitions
- Throughput: Each new partition gets 3000 RCU, 1000 WCU

Adaptive capacity:
- Detect hot partition (exceeds provisioned throughput)
- Isolate hot keys to dedicated capacity
- Automatically increase throughput for hot partition

On-Demand mode:
- No provisioned throughput limits
- Auto-scales to handle any load
- Pay per request (more expensive than provisioned)
Feature Details
Consistency Eventually consistent (default) or strongly consistent reads
Replication 3 replicas across AZs (synchronous)
Global tables Multi-region replication (async, last-write-wins)
Indexing LSI (local, same partition key), GSI (global, different partition key)

11.2 Cassandra Token Ring

Architecture

  • Token range: 2^64 hash space (Murmur3 hash)
  • Virtual nodes: 256 vnodes per physical node (default)
  • Replication: RF=3 (3 replicas), placed on next N nodes clockwise
  • Consistency: Tunable quorum (CL=QUORUM: R+W>RF)
graph TD subgraph "Cassandra Token Ring" R[Token Ring
0 to 2^64-1] R --> N1[Node 1
Tokens: 0, 256, 512, ...] R --> N2[Node 2
Tokens: 64, 320, 576, ...] R --> N3[Node 3
Tokens: 128, 384, 640, ...] R --> N4[Node 4
Tokens: 192, 448, 704, ...] K[Key X
hash=300] -->|Primary| N2 K -->|Replica 1| N3 K -->|Replica 2| N4 end style R fill:#ffd43b style N2 fill:#339af0 style N3 fill:#51cf66 style N4 fill:#51cf66

Read/Write Path

Write (CL=QUORUM, RF=3):
1. Client sends write to coordinator (any node)
2. Coordinator hashes key β†’ determines 3 replicas
3. Coordinator forwards write to all 3 replicas (parallel)
4. Waits for 2/3 ACKs (quorum)
5. Returns success to client

Read (CL=QUORUM, RF=3):
1. Coordinator sends read to all 3 replicas (parallel)
2. Waits for 2/3 responses (quorum)
3. Compares timestamps β†’ returns latest value
4. Background read-repair: send latest value to stale replica

Consistency guarantee: R+W>RF (2+2>3) β†’ strong consistency
Feature Details
Consistency levels ONE, QUORUM, ALL, LOCAL_QUORUM (multi-DC)
Conflict resolution Last-write-wins (timestamp-based)
Tombstones Soft deletes (marker for 10 days by default, then compacted)
Compaction Merge SSTables, remove tombstones (STCS, LCS, TWCS strategies)

11.3 MongoDB Chunks & Balancer

Architecture

  • Chunk: Contiguous range of shard keys (default 64MB)
  • Config servers: 3-node replica set storing chunk mappings
  • Mongos: Query router (stateless, determines which shard to query)
  • Shards: Replica sets storing actual data
graph TD subgraph "MongoDB Sharded Cluster" APP[Application] --> M1[Mongos 1
Query Router] APP --> M2[Mongos 2
Query Router] M1 --> CS[Config Servers
Chunk→Shard mappings] M2 --> CS M1 --> S0[Shard 0
Replica Set
Chunks 0-5] M1 --> S1[Shard 1
Replica Set
Chunks 6-11] M1 --> S2[Shard 2
Replica Set
Chunks 12-17] S0 --> R0[Primary] S0 --> R01[Secondary] S0 --> R02[Secondary] end style APP fill:#ffd43b style CS fill:#ff6b6b,color:#1a1a2e style S0 fill:#339af0 style S1 fill:#339af0 style S2 fill:#339af0

Balancer Operation

Initial state:
- Shard 0: 20 chunks (10GB)
- Shard 1: 10 chunks (5GB)
- Shard 2: 10 chunks (5GB)

Balancer detects imbalance: Shard 0 has 10 more chunks than Shard 1

Migration:
1. Balancer picks chunk to move (e.g., chunk 5)
2. Shard 1 (destination) starts cloning chunk 5 from Shard 0
3. While cloning, Shard 0 tracks new writes (oplog)
4. After clone complete, apply incremental changes
5. Atomic cutover: update config server (chunk 5 now on Shard 1)
6. Delete chunk 5 from Shard 0

Final state:
- Shard 0: 19 chunks
- Shard 1: 11 chunks
- Shard 2: 10 chunks

Balancer continues until all shards within threshold (Β±2 chunks)
Feature Details
Chunk splitting Auto-split at 64MB (configurable), manual via sh.splitAt()
Balancer window Run 24/7 or configure time window (e.g., off-peak only)
Jumbo chunks Chunk >64MB with all same shard key β†’ can't split, must refine shard key
Zone sharding Pin chunks to specific shards (e.g., EU data on EU shard)

11.4 Elasticsearch Shards & Replicas

Architecture

  • Shard: Lucene index (immutable, fixed count per index)
  • Replica: Copy of shard (for HA, read scaling)
  • Routing: hash(document_id) % num_primary_shards
  • Allocation: Master node assigns shards to data nodes
graph TD subgraph "Elasticsearch Cluster" IDX[Index: logs
5 primary shards
1 replica per shard] IDX --> N1[Node 1
P0, R1, R2] IDX --> N2[Node 2
P1, R2, R3] IDX --> N3[Node 3
P2, R3, R4] IDX --> N4[Node 4
P3, R4, R0] IDX --> N5[Node 5
P4, R0, R1] N1 --> P0[P0: Primary Shard 0] N1 --> R1[R1: Replica of Shard 1] N1 --> R2[R2: Replica of Shard 2] end style IDX fill:#ffd43b style N1 fill:#339af0 style N2 fill:#339af0 style N3 fill:#339af0

Index vs Search

Indexing a document:
1. Client sends doc to coordinating node
2. Coordinator hashes doc ID β†’ determines primary shard (e.g., P2)
3. Coordinator routes doc to node hosting P2
4. P2 indexes doc (adds to Lucene segment)
5. P2 replicates to all replicas (R2 on Node 1)
6. Returns success after all replicas ACK

Searching:
1. Client sends query to coordinating node
2. Coordinator broadcasts query to one copy of each shard (P0, P1, P2, P3, P4)
   - May query primary or replica (load balancing)
3. Each shard executes query, returns top N results + scores
4. Coordinator merges results, sorts, returns top N globally

Result: Every query hits all shards (scatter-gather)
Feature Details
Shard sizing Recommended: 10-50GB per shard, 20 shards per GB RAM
Over-sharding penalty Too many shards: high overhead (every query hits all shards)
Under-sharding penalty Too few shards: can't scale beyond shard count (need reindex)
Rebalancing Automatic when adding nodes (move shards to balance cluster)

11.5 Vitess Tablets (MySQL Sharding)

Architecture

  • Tablet: MySQL instance + VTTablet process (query rewriting, pooling)
  • Keyspace: Logical database (contains sharded tables)
  • Shard: Horizontal partition of keyspace
  • VTGate: Query router (stateless, MySQL protocol compatible)
graph TD subgraph "Vitess Architecture" APP[Application
MySQL Client] --> VG1[VTGate 1] APP --> VG2[VTGate 2] VG1 --> TOPO[Topology Service
etcd/ZooKeeper
Shard mappings] VG2 --> TOPO VG1 --> T0[Tablet 0
Shard -80
MySQL + VTTablet] VG1 --> T1[Tablet 1
Shard 80-
MySQL + VTTablet] T0 --> M0[MySQL Master] T0 --> R0[MySQL Replica] T1 --> M1[MySQL Master] T1 --> R1[MySQL Replica] end style APP fill:#ffd43b style TOPO fill:#ff6b6b,color:#1a1a2e style T0 fill:#339af0 style T1 fill:#339af0

Resharding in Vitess

Initial: 1 shard (entire keyspace)
Shard: -  (all key ranges)

Goal: Split to 2 shards
Shard: -80 (keys 0x00 to 0x7F)
Shard: 80- (keys 0x80 to 0xFF)

Resharding steps:
1. vtctlclient CopySchemaShard: Create schema on new shards
2. vtctlclient SplitClone: Copy data from source to destinations
   - Chunks copied in batches (10K rows/batch)
   - Rate-limited to avoid impacting prod
3. Filtered replication: Stream ongoing writes from source to destinations
4. Validation: Compare row counts, checksums
5. vtctlclient SwitchReads: Direct reads to new shards (gradual rollout)
6. vtctlclient SwitchWrites: Direct writes to new shards (atomic cutover)
7. vtctlclient DeleteShard: Drop old shard after validation

Result: Zero downtime, transparent to application
Feature Details
Query rewriting VTGate rewrites SQL to target correct shards (scatter-gather for joins)
Connection pooling VTTablet pools connections to MySQL (reduce connection overhead)
VSchema Defines sharding scheme (partition key, vindexes for secondary indexes)
Transactions 2PC for cross-shard TXs (opt-in, expensive)

System Comparison

System Partitioning Rebalancing Consistency Best For
DynamoDB Hash (partition key) Auto-split at 10GB Eventual or strong (per read) Key-value, serverless
Cassandra Consistent hashing (token ring) Add/remove nodes (minimal movement) Tunable quorum Wide-column, high availability
MongoDB Range or hash (shard key) Balancer moves chunks Strong (primary reads) Document store, flexible schema
Elasticsearch Hash (document ID) Automatic shard rebalancing Eventual Search, analytics
Vitess Range or hash (VSchema) SplitClone + filtered replication Strong (MySQL ACID) MySQL sharding at scale
Interview Tip: When asked "How does [system] partition data?":
  • Describe partitioning strategy (range, hash, consistent hashing)
  • Explain routing mechanism (how client finds the right shard)
  • Discuss rebalancing (how system handles adding/removing nodes)
  • Mention trade-offs (consistency, latency, scalability)

Chapter Summary

Key Takeaways

When to Partition

  • Dataset too large for single node (>2TB typical)
  • Write/read throughput exceeds single node capacity
  • Query latency suffers (working set doesn't fit in RAM)

Partitioning Strategies

  • Range: Good for time-series, range queries; risk of hot spots
  • Hash: Uniform distribution; no range queries, rebalancing costly
  • Compound: Best of both (DynamoDB model)
  • Consistent hashing: Minimal data movement when adding nodes

Challenges & Solutions

  • Hot spots: Salting, splitting, adaptive capacity
  • Secondary indexes: Local (scatter-gather) vs global (fast reads, eventual consistency)
  • Cross-shard queries: Denormalization, materialized views, avoid JOINs
  • Cross-shard TXs: 2PC (slow), Saga (eventual), Spanner (expensive)

Shard Key Selection

  • High cardinality, uniform distribution
  • Matches primary query pattern (minimize cross-shard queries)
  • Stable over time (avoid resharding)
  • Test with production data distribution

Rebalancing

  • Fixed partitions: Simple, must size upfront (Elasticsearch, Riak)
  • Dynamic: Adapts to growth, cold start problem (HBase, MongoDB)
  • Proportional: Scales with nodes, more complex (some cloud DBs)

Migration

  • Zero downtime: backfill, double-write, gradual cutover
  • Validation critical: row counts, checksums, shadow traffic
  • Rollback plan: how to revert if issues detected
  • Monitor: data sync lag, error rate, query latency

Interview Checklist: Sharding Discussion

  1. Identify need for sharding (capacity math)
  2. Choose partitioning strategy (range vs hash vs compound)
  3. Select shard key (cardinality, distribution, access pattern)
  4. Handle hot spots (salting, splitting, monitoring)
  5. Design secondary indexes (local vs global, trade-offs)
  6. Minimize cross-shard queries (denormalization, co-location)
  7. Plan rebalancing (fixed, dynamic, migration strategy)
  8. Real-world example (DynamoDB, Cassandra, MongoDB, etc.)