Skip to main content

Command Palette

Search for a command to run...

Database Partitioning Sharding

Published
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Database Partitioning and Sharding: A Modern Developer's Guide

Metadata

{
  "seo_title": "Database Partitioning & Sharding: TypeScript Implementation Guide",
  "meta_description": "Learn database partitioning and sharding strategies with modern TypeScript examples. Discover implementation patterns, common pitfalls, and best practices for 2026.",
  "keywords": [
    "database sharding",
    "database partitioning",
    "horizontal partitioning",
    "TypeScript database scaling",
    "distributed databases",
    "sharding strategies",
    "database architecture",
    "scalability patterns"
  ],
  "tags": [
    "Database Architecture",
    "Sharding",
    "TypeScript",
    "Scalability",
    "Distributed Systems",
    "Performance Optimization",
    "Backend Development"
  ]
}

The Problem: When Your Database Becomes the Bottleneck

In 2026, applications handle unprecedented data volumes. Your startup's user base exploded from 10,000 to 10 million users overnight. Your e-commerce platform processes millions of transactions daily. Your SaaS application stores terabytes of time-series data. Suddenly, your once-responsive database crawls to a halt.

You've already optimized queries, added indexes, and upgraded to a beefier server. Yet, you're still hitting walls:

Performance degradation: Query response times increase exponentially as tables grow beyond hundreds of millions of rows. Even indexed queries slow down because indexes themselves become massive.

Storage limitations: Single-server databases have physical storage limits. Even cloud providers impose constraints on individual database instances.

Maintenance windows: Backing up a 5TB database takes hours. Schema migrations lock tables for extended periods, causing unacceptable downtime.

Geographic latency: Users in Asia experience 200ms+ latency when your database sits in a US data center.

Write bottlenecks: A single database server can only handle so many concurrent writes, regardless of read replicas.

Cost inefficiency: Vertical scaling (bigger servers) becomes exponentially expensive and eventually hits hard limits.

Traditional solutions like read replicas and caching help with read-heavy workloads but don't address the fundamental issue: your data has outgrown a single database instance. You need horizontal scaling through partitioning and sharding.

Understanding Partitioning vs. Sharding

While often used interchangeably, these terms have distinct meanings:

Partitioning divides a large table into smaller, more manageable pieces within the same database instance. Think of it as organizing a massive filing cabinet into labeled sections.

Sharding distributes data across multiple database instances (shards), each running on separate servers. It's like having multiple filing cabinets in different offices.

Both techniques split data, but sharding provides true horizontal scalability by distributing load across multiple machines.

Modern TypeScript Solution

Let's build a production-ready sharding implementation using TypeScript, focusing on a multi-tenant SaaS application where we'll shard by tenant ID.

Architecture Overview

// types/sharding.types.ts
export interface ShardConfig {
  id: string;
  host: string;
  port: number;
  database: string;
  minKey: number;
  maxKey: number;
}

export interface ShardingStrategy {
  determineShardId(key: string | number): string;
  getAllShardIds(): string[];
}

export interface ShardedQuery<T> {
  shardKey: string | number;
  query: string;
  params?: any[];
}

Implementing a Consistent Hash Ring

Consistent hashing minimizes data movement when adding or removing shards:

// sharding/consistent-hash.ts
import crypto from 'crypto';

export class ConsistentHashRing implements ShardingStrategy {
  private ring: Map<number, string> = new Map();
  private sortedHashes: number[] = [];
  private readonly virtualNodes = 150; // Replicas per shard

  constructor(private shards: ShardConfig[]) {
    this.buildRing();
  }

  private buildRing(): void {
    for (const shard of this.shards) {
      for (let i = 0; i < this.virtualNodes; i++) {
        const hash = this.hash(`${shard.id}:${i}`);
        this.ring.set(hash, shard.id);
        this.sortedHashes.push(hash);
      }
    }
    this.sortedHashes.sort((a, b) => a - b);
  }

  private hash(key: string): number {
    return parseInt(
      crypto.createHash('md5').update(key).digest('hex').substring(0, 8),
      16
    );
  }

  determineShardId(key: string | number): string {
    const keyHash = this.hash(String(key));

    // Binary search for the first hash >= keyHash
    let left = 0;
    let right = this.sortedHashes.length - 1;

    while (left < right) {
      const mid = Math.floor((left + right) / 2);
      if (this.sortedHashes[mid] < keyHash) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }

    const targetHash = this.sortedHashes[left] || this.sortedHashes[0];
    return this.ring.get(targetHash)!;
  }

  getAllShardIds(): string[] {
    return [...new Set(this.ring.values())];
  }

  addShard(shard: ShardConfig): void {
    this.shards.push(shard);
    for (let i = 0; i < this.virtualNodes; i++) {
      const hash = this.hash(`${shard.id}:${i}`);
      this.ring.set(hash, shard.id);
      this.sortedHashes.push(hash);
    }
    this.sortedHashes.sort((a, b) => a - b);
  }
}

Shard Manager with Connection Pooling

// sharding/shard-manager.ts
import { Pool } from 'pg';

export class ShardManager {
  private pools: Map<string, Pool> = new Map();

  constructor(
    private shards: ShardConfig[],
    private strategy: ShardingStrategy
  ) {
    this.initializePools();
  }

  private initializePools(): void {
    for (const shard of this.shards) {
      this.pools.set(
        shard.id,
        new Pool({
          host: shard.host,
          port: shard.port,
          database: shard.database,
          max: 20,
          idleTimeoutMillis: 30000,
          connectionTimeoutMillis: 2000,
        })
      );
    }
  }

  async query<T>(shardedQuery: ShardedQuery<T>): Promise<T[]> {
    const shardId = this.strategy.determineShardId(shardedQuery.shardKey);
    const pool = this.pools.get(shardId);

    if (!pool) {
      throw new Error(`Shard ${shardId} not found`);
    }

    const result = await pool.query(shardedQuery.query, shardedQuery.params);
    return result.rows;
  }

  async queryAllShards<T>(query: string, params?: any[]): Promise<T[]> {
    const promises = this.strategy.getAllShardIds().map(async (shardId) => {
      const pool = this.pools.get(shardId);
      if (!pool) return [];
      const result = await pool.query(query, params);
      return result.rows;
    });

    const results = await Promise.all(promises);
    return results.flat();
  }

  async transaction<T>(
    shardKey: string | number,
    callback: (client: any) => Promise<T>
  ): Promise<T> {
    const shardId = this.strategy.determineShardId(shardKey);
    const pool = this.pools.get(shardId);

    if (!pool) {
      throw new Error(`Shard ${shardId} not found`);
    }

    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const result = await callback(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async close(): Promise<void> {
    await Promise.all(
      Array.from(this.pools.values()).map(pool => pool.end())
    );
  }
}

Practical Usage Example

// Example: Multi-tenant application
import { ShardManager } from './sharding/shard-manager';
import { ConsistentHashRing } from './sharding/consistent-hash';

const shardConfigs: ShardConfig[] = [
  { id: 'shard-1', host: 'db1.example.com', port: 5432, database: 'app_shard_1', minKey: 0, maxKey: 0 },
  { id: 'shard-2', host: 'db2.example.com', port: 5432, database: 'app_shard_2', minKey: 0, maxKey: 0 },
  { id: 'shard-3', host: 'db3.example.com', port: 5432, database: 'app_shard_3', minKey: 0, maxKey: 0 },
];

const strategy = new ConsistentHashRing(shardConfigs);
const shardManager = new ShardManager(shardConfigs, strategy);

// Insert user data
async function createUser(tenantId: string, userData: any) {
  return shardManager.query({
    shardKey: tenantId,
    query: 'INSERT INTO users (tenant_id, name, email) VALUES ($1, $2, $3) RETURNING *',
    params: [tenantId, userData.name, userData.email],
  });
}

// Query specific tenant
async function getUsersByTenant(tenantId: string) {
  return shardManager.query({
    shardKey: tenantId,
    query: 'SELECT * FROM users WHERE tenant_id = $1',
    params: [tenantId],
  });
}

// Global query across all shards
async function getTotalUserCount() {
  const results = await shardManager.queryAllShards<{ count: string }>(
    'SELECT COUNT(*) as count FROM users'
  );
  return results.reduce((sum, row) => sum + parseInt(row.count), 0);
}

Common Pitfalls and How to Avoid Them

Cross-shard queries: Joins across shards are expensive or impossible. Design your schema to keep related data on the same shard. Use denormalization strategically.

Hotspots: Poor shard key selection can create uneven load distribution. Avoid sequential IDs or timestamps as shard keys. Monitor shard metrics and rebalance if needed.

Distributed transactions: Two-phase commits across shards are complex and slow. Design for eventual consistency where possible. Use saga patterns for complex workflows.

Schema migrations: Coordinating schema changes across shards requires careful planning. Use versioned migrations and deploy changes gradually with backward compatibility.

Shard key immutability: Changing a record's shard key requires moving data between shards. Choose shard keys that won't change (tenant ID, user ID, not email addresses).

Best Practices

Choose the right shard key: Select a key with high cardinality that distributes data evenly. For multi-tenant apps, tenant ID is ideal. For social networks, user ID works well.

Implement monitoring: Track query latency, connection pool usage, and data distribution per shard. Set up alerts for imbalanced shards.

Plan for resharding: Build tooling to migrate data between shards. Test resharding procedures regularly in staging environments.

Use global tables: Maintain small, frequently-accessed reference data (countries, categories) in all shards or a separate global database.

Implement circuit breakers: Protect your application from cascading failures when a shard becomes unavailable.

Document shard topology: Maintain clear documentation of which data lives where. Use infrastructure-as-code for shard configuration.

Frequently Asked Questions

Q: When should I implement sharding? A: Consider sharding when a single database exceeds 100GB, query performance degrades despite optimization, or you need geographic distribution. Start with vertical scaling and read replicas first.

Q: Can I shard an existing application? A: Yes, but it requires significant refactoring. Plan a gradual migration: implement the sharding layer, migrate data incrementally, and update application code to use shard-aware queries.

Q: How many shards should I start with? A: Start with 3-4 shards to allow for growth without over-complicating operations. Plan for 2-3x current capacity. Adding shards later requires resharding.

Q: What about database-native sharding solutions? A: PostgreSQL's Citus, MongoDB's built-in sharding, and Vitess for MySQL offer managed sharding. They reduce implementation complexity but may limit flexibility. Evaluate based on your specific needs.

Q: How do I handle analytics queries across shards? A: Use a separate analytics database. Stream changes from shards to a data warehouse (Snowflake, BigQuery) using CDC (Change Data Capture) tools like Debezium.

Q: What happens when a shard fails? A: Implement replication within each shard (primary-replica setup). Use automatic failover mechanisms. Design your application to gracefully degrade when a shard is unavailable.

Q: Can I use different databases for different shards? A: Technically yes, but it complicates operations significantly. Stick with the same database system across shards unless you have compelling reasons (like geographic compliance requirements).

Sharding is a powerful technique for scaling databases horizontally, but it introduces operational complexity. Implement it when simpler solutions no longer suffice, and invest in robust tooling and monitoring to manage the distributed nature of your data.