Back to Sharding & Partitioning
Q3Question 3 of 7

Common sharding strategies and choosing a shard key

Key Concepts
  • Hash-based sharding hashes the shard key for even distribution, but range queries must scatter-gather across every shard.
  • Range-based sharding assigns contiguous key ranges, making range scans efficient but risking hot partitions with monotonically increasing keys.
  • Directory-based sharding keeps a lookup table mapping each key to its shard, offering maximum flexibility at the cost of an extra lookup on every access.
  • Consistent hashing with virtual nodes limits data movement when adding or removing shards to roughly 1/N of the keys.
  • A good shard key has high cardinality, distributes evenly, and aligns with the dominant query pattern so most queries hit a single shard.
  • The shard key is very hard to change later, so query patterns must be modeled before choosing it.
  • Composite shard keys can address multiple access patterns when a single key is insufficient.

Definition

Hash spreads evenly but kills range scans; range enables scans but risks hotspots; directory is flexible but adds a lookup: pick the shard key by cardinality, evenness, and query pattern.
  • Hash-based – shard = hash(key) % N or consistent hashing. Even distribution, fast single-key lookups, but range queries scatter-gather across all shards.
  • Range-based – Contiguous key ranges per shard (date blocks, ID ranges). Easy range scans, but monotonically increasing keys create a hot shard.
  • Directory-based – A lookup table maps each key to its shard. Maximum flexibility and granular rebalancing, but an extra lookup on every access.
  • Shard key criteria – High cardinality, even distribution, aligned with the dominant query pattern.

National Bank of India has too many accounts for one database, so it spreads them across several. Hash-based is like assigning accounts to branches by a scrambling formula so every branch is equally full. Range-based is like branch A keeps accounts 1 to a million: easy to grab a range, but if all new accounts land in the last branch it gets overloaded. Directory-based is like a central register that says exactly which branch holds Rajesh's account: flexible but you always consult the register first.

Understanding the Concepts

What is hash-based sharding?

Hash-based sharding computes shard = hash(key) % N to distribute data evenly across N shards. Single-key lookups are fast because you compute the shard directly. But range queries must scatter-gather across every shard.

JAVA
package com.bank.sharding;

public class HashShardRouter {

    private final int shardCount;

    public HashShardRouter(int shardCount) {
        this.shardCount = shardCount;
    }

    public int getShard(String accountId) {
        int hash = Math.abs(accountId.hashCode());
        return hash % shardCount;
    }

    public static void main(String[] args) {
        HashShardRouter router = new HashShardRouter(4);
        System.out.println("SAV2001 → Shard " + router.getShard("SAV2001"));
        System.out.println("SAV2002 → Shard " + router.getShard("SAV2002"));
        System.out.println("SAV2003 → Shard " + router.getShard("SAV2003"));
        // Even distribution across 4 shards
    }
}

The problem with plain modulo: when N changes (add a shard), almost every key moves to a different shard. Consistent hashing fixes this.

JAVA
package com.bank.sharding;

import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHashRouter {

    private final SortedMap<Integer, String> ring = new TreeMap<>();
    private final int virtualNodes;

    public ConsistentHashRouter(int virtualNodes) {
        this.virtualNodes = virtualNodes;
    }

    public void addShard(String shardName) {
        for (int i = 0; i < virtualNodes; i++) {
            int hash = (shardName + "-vn" + i).hashCode();
            ring.put(hash, shardName);
        }
    }

    public String getShard(String key) {
        int hash = key.hashCode();
        SortedMap<Integer, String> tail = ring.tailMap(hash);
        int nodeHash = tail.isEmpty() ? ring.firstKey() : tail.firstKey();
        return ring.get(nodeHash);
    }

    public static void main(String[] args) {
        ConsistentHashRouter router = new ConsistentHashRouter(100);
        router.addShard("shard-0");
        router.addShard("shard-1");
        router.addShard("shard-2");

        System.out.println("SAV2001 → " + router.getShard("SAV2001"));
        System.out.println("SAV2002 → " + router.getShard("SAV2002"));

        // Adding shard-3 moves only ~1/4 of keys, not all
        router.addShard("shard-3");
        System.out.println("After adding shard-3:");
        System.out.println("SAV2001 → " + router.getShard("SAV2001"));
    }
}

What is range-based sharding?

Range-based sharding assigns contiguous key ranges to each shard. Range scans and time-window queries stay on few shards.

JAVA
package com.bank.sharding;

public class RangeShardRouter {

    // Shard 0: account IDs A000000 - A999999
    // Shard 1: account IDs B000000 - B999999
    // Shard 2: account IDs C000000 - C999999

    public int getShard(String accountId) {
        char prefix = accountId.charAt(0);
        if (prefix <= 'A') return 0;
        if (prefix <= 'B') return 1;
        return 2;
    }

    // Risk: if all new accounts start with 'C',
    // Shard 2 becomes a hot spot
    // Monotonically increasing IDs always land on the last shard
}
DIAGRAM
Range-based with date sharding (transactions):
  Shard 0: Jan-Mar 2024
  Shard 1: Apr-Jun 2024
  Shard 2: Jul-Sep 2024
  Shard 3: Oct-Dec 2024

  Query: WHERE txn_date BETWEEN '2024-04-01' AND '2024-06-30'
  → Only hits Shard 1 (efficient!)

  Risk: Shard 3 gets ALL current writes (hot partition)

What is directory-based sharding?

Directory-based sharding maintains a lookup table that maps each key to its shard. This gives maximum flexibility but adds a lookup dependency.

JAVA
package com.bank.sharding;

import java.util.HashMap;
import java.util.Map;

public class DirectoryShardRouter {

    // Lookup table: key → shard mapping
    private final Map<String, Integer> directory = new HashMap<>();

    public void registerAccount(String accountId, int shardId) {
        directory.put(accountId, shardId);
    }

    public int getShard(String accountId) {
        Integer shard = directory.get(accountId);
        if (shard == null) {
            throw new IllegalArgumentException("Unknown account: " + accountId);
        }
        return shard;
    }

    // Pros: can rebalance by changing the directory entry
    //       maximum flexibility in placement
    // Cons: extra lookup on every access
    //       directory must be highly available and cached
    //       directory itself can become a bottleneck

    public static void main(String[] args) {
        DirectoryShardRouter router = new DirectoryShardRouter();
        router.registerAccount("SAV2001", 0);
        router.registerAccount("SAV2002", 1);
        router.registerAccount("SAV2003", 0);  // Can place anywhere

        System.out.println("SAV2001 → Shard " + router.getShard("SAV2001"));
        System.out.println("SAV2002 → Shard " + router.getShard("SAV2002"));
    }
}

How do the strategies compare?

TEXT
| Feature           | Hash-based          | Range-based        | Directory-based     |
|-------------------|---------------------|--------------------|---------------------|
| Distribution      | Even                | Depends on ranges  | Flexible            |
| Single-key lookup | Fast (compute hash) | Fast (find range)  | Lookup required     |
| Range queries     | Scatter-gather      | Efficient          | Lookup per key      |
| Hot spots         | Unlikely            | Likely (monotonic) | Controllable        |
| Adding shards     | Consistent hashing  | Add new range      | Update directory    |
| Rebalancing       | Move ~1/N keys      | Hard (split range) | Granular (per key)  |
| Complexity        | Low                 | Low                | Medium (directory)  |

How do you choose the shard key?

The shard key determines how data is distributed across shards. A bad choice creates hot spots that are painful to fix because re-sharding moves enormous amounts of data.

JAVA
package com.bank.sharding;

public class ShardKeyDecision {

    // Rule 1: High cardinality
    // GOOD: account_id (millions of unique values)
    // BAD:  branch_city (only 50 cities → uneven distribution)

    // Rule 2: Even distribution
    // GOOD: account_id with hash (spreads evenly)
    // BAD:  created_date (all current writes hit one shard)

    // Rule 3: Aligned with dominant query
    // Dominant query: "get all transactions for account SAV2001"
    // GOOD: shard by account_id → single shard hit
    // BAD:  shard by txn_date → scatter-gather across all shards

    // Rule 4: Avoid cross-shard joins
    // If accounts and transactions are on different shards,
    // joining them requires network calls between shards
    // GOOD: co-locate account + its transactions on the same shard

    // Rule 5: Very hard to change later
    // Model your query patterns BEFORE choosing the shard key
    // Re-sharding moves enormous amounts of data
}

What about composite shard keys?

When a single key does not satisfy multiple access patterns, use a composite key.

DIAGRAM
Composite shard key example:
  Shard by: hash(account_id) + range(txn_date)

  Query 1: "All transactions for SAV2001 in March"
  → hash(SAV2001) finds the shard, date range filters within it
  → Single shard hit ✓

  Query 2: "All transactions today across all accounts"
  → Must scatter-gather (still not great)
  → But better than sharding by date alone

What are common mistakes when choosing a shard key?

The most common mistake is choosing a shard key that creates hot spots. For example, sharding by creation date sends all new writes to the same shard. Another mistake is choosing a key that does not appear in most queries, forcing scatter-gather on every read.

SQL
-- WRONG: sharding by created_date sends all new rows to the latest shard
-- Hot spot: one shard handles all current writes

-- CORRECT: sharding by customer_id distributes writes evenly
-- Most queries filter by customer_id, so they stay shard-local
SELECT * FROM transactions
WHERE customer_id = 42 AND created_at > '2024-01-01';

What should a senior engineer know?

A senior engineer should plan resharding from day one using consistent hashing with virtual nodes, model all query patterns before choosing a shard key, and understand that the shard key decision is effectively permanent.

TEXT
Senior shard key checklist:
1. List all dominant query patterns
2. Pick the key that most queries filter on
3. Verify high cardinality and even distribution
4. Co-locate related data on the same shard
5. Use consistent hashing with virtual nodes
6. Plan resharding strategy from day one
7. Test with realistic data distribution before production

Visual Overview

DIAGRAM
┌────────────────────────────────────────────────────────────┐
│            Sharding Strategies Comparison                 │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Hash-based:                                              │
│  Key → hash() → shard                                    │
│  [Even] [Fast lookup] [Bad range queries]                 │
│                                                            │
│  Range-based:                                             │
│  Key range → shard                                        │
│  [Good range scans] [Hot last shard] [Easy to add]        │
│                                                            │
│  Directory-based:                                         │
│  Key → lookup table → shard                               │
│  [Flexible] [Granular rebalance] [Extra lookup]           │
│                                                            │
│  Shard Key Selection:                                     │
│  ┌─────────────────────────────────────────────┐          │
│  │ High cardinality (millions of values)       │          │
│  │ Even distribution (no hot spots)            │          │
│  │ Aligned with dominant query pattern         │          │
│  │ Very hard to change later!                  │          │
│  └─────────────────────────────────────────────┘          │
└────────────────────────────────────────────────────────────┘

Key Takeaway

Sharding splits one logical dataset across many physical databases, and the three common strategies trade off differently. Hash-based sharding hashes the shard key, usually with consistent hashing, giving even distribution and fast single-key lookups, but range queries must scatter-gather across every shard. Range-based sharding assigns contiguous key ranges so range scans stay on a few shards, but monotonically increasing keys create a hot shard. Directory-based sharding keeps a lookup table for maximum flexibility at the cost of an extra lookup on every access.

Choosing the shard key is the most consequential decision because it is very hard to change later. A good shard key has high cardinality so data spreads out, distributes evenly to avoid hotspots, and aligns with the dominant query so most requests hit a single shard. When a single key does not satisfy multiple access patterns, a composite key can help.

At the National Bank of India, the overwhelming access pattern is lookup by account_id, so Pankaj shards by account_id using consistent hashing with virtual nodes, which spreads load evenly and keeps single-account reads on one shard. He avoids keys like branch_city that would skew badly, and plans resharding from day one.

Remember one sentence: hash spreads evenly but hurts range scans, range enables scans but risks hotspots, and directory is flexible but adds a lookup, so choose the shard key by cardinality, evenness, and query pattern.

---

Share
Use ← → arrow keys to navigate