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
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
| 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
| 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) |
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)
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
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).
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.
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]
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 |
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).
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.
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
- 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
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)
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.
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) |
- 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.
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).
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.
- 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
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
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
- "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.
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.
(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.
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 |
- "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.
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.
| 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
- 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
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)
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
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
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)
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 |
- 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
- Identify need for sharding (capacity math)
- Choose partitioning strategy (range vs hash vs compound)
- Select shard key (cardinality, distribution, access pattern)
- Handle hot spots (salting, splitting, monitoring)
- Design secondary indexes (local vs global, trade-offs)
- Minimize cross-shard queries (denormalization, co-location)
- Plan rebalancing (fixed, dynamic, migration strategy)
- Real-world example (DynamoDB, Cassandra, MongoDB, etc.)