Back to Sharding & Partitioning
Q1Question 1 of 7

The correct order of database scaling steps, and why sharding is last

Key Concepts
  • The correct scaling order moves from cheapest and least disruptive to most expensive and most disruptive.
  • Optimize queries and indexes first: a missing index turning a scan into a seek can cut a query a thousandfold, at zero infrastructure cost.
  • Scale up vertically next: more CPU, memory, and faster SSD on one machine, with zero code changes.
  • Add read replicas to offload read-heavy traffic from the primary, since banking workloads are typically read-heavy.
  • Add a cache like Redis for hot read-mostly data, serving it from memory in microseconds.
  • Apply CQRS to split read and write models, letting each scale independently.
  • Shard last because it permanently breaks cross-node joins, transactions need 2PC or Sagas, and re-sharding is operationally painful.

Definition

Optimize, scale up, replicate, cache, CQRS, then shard: because sharding is a one-way door that breaks joins and transactions.
  • Step 1: Optimize – Fix queries, add indexes, refresh statistics. Zero infrastructure cost, often the entire fix.
  • Step 2: Scale up – More CPU, memory, faster storage. Zero code changes, large headroom.
  • Step 3: Read replicas – Offload reads to replicas, freeing the primary for writes.
  • Step 4: Cache – Serve hot data from Redis in microseconds, removing load from the database.
  • Step 5: CQRS – Separate read and write models, each scaled independently.
  • Step 6: Shard – Split data across nodes. Last resort because it breaks joins, transactions, and is effectively a one-way door.

When the National Bank branch gets crowded, you first make the clerks work smarter (fix slow processes and indexes), then give them a faster desk (bigger machine), then add read-only clerks for balance enquiries (replicas), then keep a notepad of common answers (cache), then separate the write counter from the read counter (CQRS). Only when none of that is enough do you split customers across separate branches (sharding), because once you do, answering 'total across all branches' and moving money between branches becomes genuinely hard.

Understanding the Concepts

Why does the order matter?

Each step is dramatically cheaper and less risky than the next, and most systems never need the last step at all. Applying them in order ensures you are not paying the complexity cost of sharding when a simple index fix would have solved the problem.

TEXT
Scaling steps by cost and complexity:

 Step  | Infrastructure Cost | Code Change | Reversible? | Risk
-------|--------------------|--------------|--------------|---------
 Index | Zero               | None         | Yes          | None
 Scale | Hardware cost      | None         | Yes          | Low
 Replica | Moderate         | Routing      | Yes          | Low
 Cache | Moderate           | Cache logic  | Yes          | Medium
 CQRS  | Significant        | Architecture | Hard         | Medium
 Shard | Very high          | Everything   | No           | High

Step 1: Optimize queries and indexes

This is free in infrastructure terms and is often the entire fix. A missing index turning a sequential scan into an index seek can cut a query a thousandfold.

SQL
-- Before: sequential scan on 10 million rows
EXPLAIN ANALYZE
SELECT * FROM transactions WHERE account_id = 'SAV2001';
-- Seq Scan: 8000ms

-- Fix: add index
CREATE INDEX idx_txn_account ON transactions(account_id);

-- After: index scan
-- Index Scan: 0.5ms (16,000x faster)
JAVA
package com.bank.maintenance;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;

public class IndexOptimizationService {

    private final DataSource dataSource;

    public IndexOptimizationService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void addMissingIndex() throws SQLException {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("CREATE INDEX CONCURRENTLY idx_txn_acct_date "
                    + "ON transactions(account_id, created_at)");
            System.out.println("Index created without locking the table");
        }
    }
}

Step 2: Scale up vertically

More CPU, memory, and faster NVMe storage on one machine. Modern hardware is enormous: a single node can handle tens of thousands of transactions per second. Zero code changes required.

DIAGRAM
Vertical scaling at National Bank of India:

Before: 4 cores, 16 GB RAM, HDD
  → 2,000 tps, working set does not fit in memory

After: 16 cores, 128 GB RAM, NVMe SSD
  → 20,000 tps, entire working set cached
  → Zero code changes, zero architecture changes

Limit: there is a biggest machine, and cost rises steeply at the top

Step 3: Add read replicas

Most OLTP workloads are read-heavy. Sending reads to replicas frees the primary to handle writes. Replicas replay the primary's WAL, so they are straightforward to set up.

JAVA
package com.bank.config;

import javax.sql.DataSource;

public class ReadWriteRoutingConfig {

    private final DataSource primaryDataSource;  // For writes
    private final DataSource replicaDataSource;  // For reads

    public ReadWriteRoutingConfig(DataSource primary, DataSource replica) {
        this.primaryDataSource = primary;
        this.replicaDataSource = replica;
    }

    // Write operations go to primary
    public DataSource getWriteDataSource() {
        return primaryDataSource;
    }

    // Read operations go to replica
    // Caveat: replication lag means reads may be slightly stale
    public DataSource getReadDataSource() {
        return replicaDataSource;
    }
}
DIAGRAM
Read replica topology:
┌─────────┐     WAL stream     ┌──────────┐
│ Primary │────────────────────→│ Replica 1│ (balance checks)
│ (writes)│────────────────────→│ Replica 2│ (reports)
└─────────┘                     └──────────┘

Step 4: Add a cache

A cache like Redis serves hot read-mostly data from memory in microseconds, removing enormous load from the database.

JAVA
package com.bank.cache;

import redis.clients.jedis.Jedis;
import java.math.BigDecimal;

public class BalanceCacheService {

    private final Jedis redis;

    public BalanceCacheService(Jedis redis) {
        this.redis = redis;
    }

    public BigDecimal getBalance(String accountId) {
        String cached = redis.get("balance:" + accountId);
        if (cached != null) {
            return new BigDecimal(cached);  // Cache hit: microseconds
        }
        return null;  // Cache miss: fall through to database
    }

    public void cacheBalance(String accountId, BigDecimal balance) {
        redis.setex("balance:" + accountId, 60, balance.toPlainString());
    }

    public void invalidateBalance(String accountId) {
        redis.del("balance:" + accountId);  // Invalidate on write
    }
}

Step 5: Apply CQRS

CQRS splits the write model from one or more read models, each shaped and scaled for its purpose. The read side can be materialized from events and served from a different store.

JAVA
package com.bank.cqrs;

import java.math.BigDecimal;

// Command side: writes to PostgreSQL (ACID)
public class TransferCommandHandler {
    public void handle(String from, String to, BigDecimal amount) {
        // Debit and credit in a single ACID transaction
        // Publish event: TransferCompleted
    }
}

// Query side: reads from a denormalized read store
public class AccountQueryHandler {
    public BigDecimal getBalance(String accountId) {
        // Read from a denormalized view or separate read database
        // Optimized for the specific query pattern
        return BigDecimal.ZERO;
    }
}

Why is sharding the last resort?

Sharding permanently changes the data model and introduces distributed-systems challenges that do not exist in a single database.

JAVA
package com.bank.sharding;

public class ShardingChallenges {

    // 1. Cross-shard joins are gone
    // "Show total balance across all accounts" now requires
    // fan-out to every shard and application-side aggregation

    // 2. Cross-shard transactions need 2PC or Sagas
    // Transfer between accounts on different shards:
    // BEGIN on Shard A: debit SAV2001
    // BEGIN on Shard B: credit SAV2002
    // Both must commit or both roll back → 2PC or Saga

    // 3. Choosing a bad shard key creates hot spots
    // Shard by city? Mumbai gets 40% of traffic
    // Re-sharding moves enormous amounts of data

    // 4. Global unique constraints and auto-increment IDs break
    // Need distributed ID generation (Snowflake, UUIDv7)

    // 5. Schema migrations must be coordinated across all shards

    // 6. Operationally: N databases to monitor, backup, and maintain

    // 7. It is effectively a one-way door: un-sharding is a major project
}

What is the key nuance about read vs write scaling?

The earlier steps mainly scale reads, while sharding is the main way to scale writes. If the bottleneck is purely read volume, replicas and cache solve it without sharding.

TEXT
Scaling dimension:

  Step           | Scales Reads | Scales Writes
  Index          | Yes          | Yes (less I/O)
  Scale up       | Yes          | Yes
  Replica        | Yes          | No (primary only)
  Cache          | Yes          | No
  CQRS           | Yes          | Indirectly
  Shard          | Yes          | YES (main purpose)

Sharding becomes necessary specifically when:
  - Write throughput exceeds a single node
  - Raw data volume exceeds a single node's storage
  - NOT when reads are the bottleneck (replicas + cache solve that)

What should a senior engineer know?

A senior engineer should resist the urge to shard prematurely. Most systems never need it. The right time to shard is when you have genuinely hit the write-throughput or data-size ceiling of a single optimized, replicated, cached node.

DIAGRAM
Senior decision framework:

  "The database is slow"
  → Is it a missing index? Fix it.
  → Is the machine undersized? Scale up.
  → Are reads overwhelming the primary? Add replicas.
  → Is the same data read repeatedly? Add cache.
  → Do reads and writes have conflicting needs? CQRS.
  → Is WRITE throughput beyond one node? NOW shard.
  → Is DATA SIZE beyond one node? NOW shard.

Visual Overview

DIAGRAM
┌────────────────────────────────────────────────────────────┐
│         Database Scaling Steps (Cheapest First)           │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  1. Optimize Queries/Indexes     [FREE]                   │
│     └→ Missing index? 16,000x faster                      │
│                                                            │
│  2. Scale Up Vertically          [$$]                     │
│     └→ More CPU, RAM, SSD. Zero code change               │
│                                                            │
│  3. Add Read Replicas            [$$$]                    │
│     └→ Offload reads. Primary handles writes only         │
│                                                            │
│  4. Add Cache (Redis)            [$$$]                    │
│     └→ Hot data in microseconds. Less DB load             │
│                                                            │
│  5. CQRS                         [$$$$]                   │
│     └→ Separate read/write models. Independent scaling    │
│                                                            │
│  6. Shard                        [$$$$$] ← LAST RESORT   │
│     └→ Breaks joins, transactions, adds ops complexity    │
│     └→ ONE-WAY DOOR: un-sharding is a major project      │
│                                                            │
│  Most systems never reach step 6.                         │
└────────────────────────────────────────────────────────────┘

Key Takeaway

The correct order of database scaling steps goes from cheapest and least disruptive to most expensive and most disruptive: optimize queries and indexes, scale up vertically, add read replicas, add a cache, apply CQRS, and only then shard. Each step is dramatically cheaper and safer than the next, and most systems never need the last step.

Optimizing indexes is free in infrastructure terms and is often the entire fix. Scaling up vertically needs zero code changes. Read replicas offload the read-heavy banking workload. A cache serves hot data in microseconds. CQRS lets read and write models scale independently.

Sharding is the last resort for concrete reasons. It permanently splits data across nodes, breaking joins into scatter-gather fan-outs, forcing cross-shard transactions into two-phase commit or Sagas, breaking auto-increment IDs, and requiring coordinated schema migrations across every shard. It is effectively a one-way door: un-sharding is a major project. The key nuance is that the earlier steps mainly scale reads, while sharding scales writes. If the bottleneck is read volume, replicas and cache solve it without sharding.

At the National Bank of India, an engineer wanted to shard the moment the database slowed, but the real cause was a missing index and a read-heavy balance-check load. An index plus read replicas and a Redis cache solved it completely. The team did not shard for years until write throughput genuinely exceeded what a single optimized node could handle.

Remember one sentence: optimize, scale up, replicate, cache, CQRS, then shard: because sharding is a one-way door that breaks joins and transactions, so exhaust every cheaper option first.

---

Share
Use ← → arrow keys to navigate