Back to Sharding & Partitioning
Q4Question 4 of 7

Challenges of sharding

Key Concepts
  • Cross-shard queries and joins require scatter-gather across all shards and lose the single-node optimizer.
  • Cross-shard transactions need two-phase commit (blocking, coordinator failure risk) or Saga patterns with compensating actions.
  • Resharding and rebalancing when adding nodes requires online data movement; consistent hashing limits movement to roughly 1/N of keys.
  • Foreign keys cannot cross shards, so referential integrity becomes an application responsibility with background reconciliation.
  • Schema migrations must be coordinated across every shard, rolled out in backward-compatible waves with version tracking.
  • Operational complexity multiplies: more nodes mean more monitoring, backups per shard, consistent configuration, and harder debugging.
  • The senior principle is to not shard prematurely: exhaust vertical scaling, read replicas, and caching first because sharding's complexity is permanent.

Definition

Sharding buys scale but charges you in cross-shard queries, distributed transactions, resharding, lost foreign keys, coordinated migrations, and operational complexity.
  • Cross-shard queries – Queries spanning shards require scatter-gather fan-out and lose the single-node optimizer, getting slower as shard count grows.
  • Cross-shard transactions – Atomic changes across shards need two-phase commit or Saga patterns with compensating actions.
  • Resharding – Adding nodes forces data movement; naive modulo reshuffles everything, consistent hashing limits it to ~1/N.
  • Lost referential integrity – Foreign keys cannot cross shards, so constraints become application responsibilities.
  • Coordinated migrations – Every shard must receive identical schema changes, rolled out in waves.
  • Operational complexity – More nodes mean more monitoring, backups, configuration, and debugging.

Once National Bank of India spreads accounts across many branches, a report covering all customers must ask every branch and stitch the answers together. A transfer between two customers in different branches needs both branches to agree atomically. Adding a new branch means physically moving some accounts. And a form change must be rolled out to every branch identically. The bank gains scale but pays in coordination.

Understanding the Concepts

What is the cross-shard query challenge?

A query not constrained to one shard must fan out to all shards and merge results in the application or a coordinator. This loses the single-node optimizer and gets slower as shard count grows.

JAVA
package com.bank.sharding;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import javax.sql.DataSource;

public class CrossShardQueryService {

    private final List<DataSource> shardDataSources;

    public CrossShardQueryService(List<DataSource> shardDataSources) {
        this.shardDataSources = shardDataSources;
    }

    // "Total balance across ALL accounts" requires scatter-gather
    public BigDecimal getTotalBalanceAcrossAllShards() throws SQLException {
        BigDecimal total = BigDecimal.ZERO;
        for (DataSource ds : shardDataSources) {
            try (Connection conn = ds.getConnection();
                 PreparedStatement ps = conn.prepareStatement(
                         "SELECT SUM(balance) FROM accounts")) {
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next() && rs.getBigDecimal(1) != null) {
                        total = total.add(rs.getBigDecimal(1));
                    }
                }
            }
        }
        return total;  // Aggregated across all shards in application
    }

    // Mitigation: denormalize, co-locate related data on same shard,
    // or use a separate analytics/search store for cross-shard queries
}

This query must contact every shard, execute a SUM on each, collect the results, and add them in the application. With 100 shards, that is 100 queries per request.

What is the cross-shard transaction challenge?

Atomic changes across shards need two-phase commit (blocking, coordinator failure risk) or a Saga of local transactions with compensating actions.

JAVA
package com.bank.sharding;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;

public class CrossShardTransferService {

    private final DataSource shardA;
    private final DataSource shardB;

    public CrossShardTransferService(DataSource shardA, DataSource shardB) {
        this.shardA = shardA;
        this.shardB = shardB;
    }

    // Saga pattern: local transactions with compensating actions
    public void transferAcrossShards(String fromAccount, String toAccount,
                                      BigDecimal amount) throws SQLException {
        // Step 1: Debit on Shard A (local transaction)
        try (Connection connA = shardA.getConnection()) {
            connA.setAutoCommit(false);
            try {
                debit(connA, fromAccount, amount);
                connA.commit();
            } catch (SQLException e) {
                connA.rollback();
                throw new RuntimeException("Debit failed, no compensation needed", e);
            }
        }

        // Step 2: Credit on Shard B (local transaction)
        try (Connection connB = shardB.getConnection()) {
            connB.setAutoCommit(false);
            try {
                credit(connB, toAccount, amount);
                connB.commit();
            } catch (SQLException e) {
                connB.rollback();
                // Step 3: Compensate - re-credit Shard A
                compensateDebit(fromAccount, amount);
                throw new RuntimeException("Credit failed, debit compensated", e);
            }
        }
    }

    private void debit(Connection conn, String accountId, BigDecimal amount)
            throws SQLException {
        try (PreparedStatement ps = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? WHERE account_id = ?")) {
            ps.setBigDecimal(1, amount);
            ps.setString(2, accountId);
            ps.executeUpdate();
        }
    }

    private void credit(Connection conn, String accountId, BigDecimal amount)
            throws SQLException {
        try (PreparedStatement ps = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE account_id = ?")) {
            ps.setBigDecimal(1, amount);
            ps.setString(2, accountId);
            ps.executeUpdate();
        }
    }

    private void compensateDebit(String accountId, BigDecimal amount)
            throws SQLException {
        try (Connection connA = shardA.getConnection()) {
            connA.setAutoCommit(false);
            credit(connA, accountId, amount);  // Reverse the debit
            connA.commit();
        }
    }
}

The Saga pattern is eventually consistent: between the debit and the credit (or compensation), the system is temporarily inconsistent. For money, this must be handled with idempotent operations and an outbox pattern.

What is the resharding challenge?

Adding capacity requires moving data between shards. Naive modulo hashing reshuffles almost everything.

DIAGRAM
Modulo hashing with 3 shards:
  key 1 → 1 % 3 = 1 (Shard 1)
  key 7 → 7 % 3 = 1 (Shard 1)
  key 8 → 8 % 3 = 2 (Shard 2)

Add shard, now 4 shards:
  key 1 → 1 % 4 = 1 (Shard 1) ← stays
  key 7 → 7 % 4 = 3 (Shard 3) ← MOVES from 1 to 3
  key 8 → 8 % 4 = 0 (Shard 0) ← MOVES from 2 to 0

~75% of keys move! For a billion-row table, that is 750M rows.

Consistent hashing with virtual nodes:
  Adding 1 shard to 3 shards moves ~1/4 of keys (~250M rows)
  Much more manageable, and done online without downtime

What about referential integrity across shards?

Foreign keys cannot cross shards. The database on Shard A has no way to verify that a foreign key references a row on Shard B.

JAVA
package com.bank.sharding;

public class ReferentialIntegrityChallenges {

    // Single database:
    // FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
    // Database enforces: no dangling references

    // Sharded:
    // accounts on Shard A, customers on Shard B
    // No foreign key enforcement across shards!
    // Application must verify references manually
    // Background reconciliation job checks for orphans

    // Mitigation: co-locate related data on the same shard
    // If customer + their accounts are on the same shard,
    // foreign key within that shard still works
}

What about schema migrations across shards?

Every shard must receive the same schema change, and during the rollout some shards have the new schema and some have the old.

DIAGRAM
Schema migration across 10 shards:

  Phase 1: Deploy backward-compatible migration
    ALTER TABLE accounts ADD COLUMN mobile VARCHAR(15);
    (NULL-able column, existing code still works)

  Phase 2: Roll out to shards in waves
    Wave 1: Shards 0-2 (migrate, verify, monitor)
    Wave 2: Shards 3-5 (migrate, verify, monitor)
    Wave 3: Shards 6-9 (migrate, verify, monitor)

  Phase 3: Update application code to use new column

  Phase 4: Make column NOT NULL (if needed)

  Track shard schema versions to detect drift:
    Shard 0: v42 ✓
    Shard 1: v42 ✓
    Shard 2: v41 ✗ ← migration failed, needs retry

What is the operational complexity?

More shards mean more of everything in operations.

JAVA
package com.bank.sharding;

public class OperationalComplexity {

    // With 10 shards:
    // - 10 databases to monitor (disk, CPU, connections, replication lag)
    // - 10 separate backup schedules
    // - 10 sets of WAL archiving to verify
    // - 10 databases to patch and upgrade
    // - 10 databases to check for slow queries
    // - Configuration must be consistent across all 10
    // - Debugging: "which shard is this row on?"
    // - Capacity planning: each shard may grow differently

    // With 1 database:
    // - 1 of everything above
    // - Much simpler

    // The operational cost is permanent and grows with shard count
}

What is the co-location mitigation?

The best way to reduce cross-shard problems is to co-locate related data on the same shard.

JAVA
package com.bank.sharding;

public class DataCoLocation {

    // Co-locate customer + accounts + transactions on the same shard
    // Shard key: customer_id
    //
    // Customer C001: Shard 2
    //   Account SAV2001: Shard 2 (same shard)
    //   Transactions for SAV2001: Shard 2 (same shard)
    //
    // Query: "All of Rajesh's transactions" → single shard
    // Transfer between Rajesh's own accounts → single shard
    //
    // Transfer between Rajesh (Shard 2) and Priya (Shard 0)
    //   → cross-shard, needs Saga
    //   → but this is less common than within-customer operations
}

What should a senior engineer know?

A senior engineer should not shard prematurely. Every challenge listed here is permanent: cross-shard joins never go away, Sagas never become simpler, and operational complexity only grows. Exhaust vertical scaling, read replicas, caching, and CQRS first.

DIAGRAM
Senior sharding checklist:
1. Have you optimized queries and indexes? (free fix)
2. Have you scaled up vertically? (zero code change)
3. Have you added read replicas? (handles read scale)
4. Have you added a cache? (handles hot reads)
5. Is the bottleneck WRITE throughput or DATA SIZE?
   YES → now consider sharding
   NO  → do not shard
6. Co-locate related data to minimize cross-shard operations
7. Plan resharding from day one (consistent hashing)
8. Accept that sharding is a one-way door

Visual Overview

DIAGRAM
┌────────────────────────────────────────────────────────────┐
│              Challenges of Sharding                       │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Cross-shard Queries:                                     │
│  ┌───────┐ ┌───────┐ ┌───────┐                           │
│  │Shard 0│ │Shard 1│ │Shard 2│  ← fan out to all         │
│  │ SUM() │ │ SUM() │ │ SUM() │  ← merge in application   │
│  └───┬───┘ └───┬───┘ └───┬───┘                           │
│      └─────────┼─────────┘                                │
│                ▼                                           │
│         Total = sum of all                                │
│                                                            │
│  Cross-shard Transaction (Saga):                          │
│  ┌───────┐              ┌───────┐                         │
│  │Shard A│  Step 1:     │Shard B│  Step 2:                │
│  │ Debit │──────────────│ Credit│                         │
│  └───────┘  If fails:   └───────┘                         │
│              Compensate → re-credit Shard A               │
│                                                            │
│  Other challenges:                                        │
│  • Resharding moves data (use consistent hashing)         │
│  • Foreign keys cannot cross shards                       │
│  • Schema migrations rolled out in waves                  │
│  • N databases to monitor, backup, and maintain            │
└────────────────────────────────────────────────────────────┘

Key Takeaway

Sharding buys horizontal scale but introduces real distributed-systems challenges that are permanent. Queries and joins not confined to one shard must scatter-gather across all shards and merge in the application, losing the single-node optimizer. Atomic changes across shards need two-phase commit or a Saga of local transactions with compensating actions: the debit happens on one shard, the credit on another, and if the credit fails, a compensation action reverses the debit. Adding nodes forces resharding, so consistent hashing with virtual nodes limits data movement to roughly 1/N of the keys.

Foreign keys cannot cross shards, so referential integrity moves into the application with background reconciliation. Schema migrations must be rolled out in backward-compatible waves to every shard with version tracking. And the whole system is operationally heavier: more nodes to monitor, back up, patch, and debug.

The best mitigation is co-locating related data on the same shard. If Rajesh's customer record, accounts, and transactions all live on the same shard, most operations stay local. Only cross-customer transfers need the Saga pattern.

At the National Bank of India, a bank-wide total balance report scatter-gathers a SUM from every shard and adds them in the coordinator. A transfer between Rajesh on one shard and Priya on another runs as a Saga: debit first, then credit, with a compensating re-credit if the credit fails. The principle Pankaj always states is to not shard prematurely: exhaust vertical scaling, replicas, and caching first, because once data is sharded the coordination cost is permanent.

Remember one sentence: sharding charges you in cross-shard queries, distributed transactions, resharding, lost foreign keys, coordinated migrations, and operational complexity.

---

Share
Use ← → arrow keys to navigate