Back to Journal
System Design

Complete Guide to Database Sharding with Typescript

A comprehensive guide to implementing Database Sharding using Typescript, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 18 min read

TypeScript's type system provides unique advantages for database sharding: type-safe shard routing, discriminated unions for shard configuration, and async/await for clean scatter-gather implementations. This guide covers building a complete sharding layer in TypeScript with Node.js, from consistent hash routing through cross-shard query execution and connection pool management.

Shard Configuration Types

typescript
1interface ShardConfig {
2 id: string;
3 host: string;
4 port: number;
5 database: string;
6 user: string;
7 password: string;
8 maxConnections: number;
9 ssl?: boolean;
10}
11 
12interface ShardRouteResult {
13 shardId: string;
14 pool: Pool;
15}
16 
17interface ScatterResult<T> {
18 shardId: string;
19 data: T[];
20 error?: Error;
21}
22 

Consistent Hash Router

typescript
1import { createHash } from 'crypto';
2 
3class ConsistentHashRouter {
4 private ring: Map<number, string> = new Map();
5 private sortedHashes: number[] = [];
6 private virtualNodes: number;
7 
8 constructor(virtualNodes: number = 150) {
9 this.virtualNodes = virtualNodes;
10 }
11 
12 addShard(shardId: string): void {
13 for (let i = 0; i < this.virtualNodes; i++) {
14 const hash = this.hash(`${shardId}:${i}`);
15 this.ring.set(hash, shardId);
16 }
17 this.sortedHashes = Array.from(this.ring.keys()).sort((a, b) => a - b);
18 }
19 
20 removeShard(shardId: string): void {
21 for (let i = 0; i < this.virtualNodes; i++) {
22 const hash = this.hash(`${shardId}:${i}`);
23 this.ring.delete(hash);
24 }
25 this.sortedHashes = Array.from(this.ring.keys()).sort((a, b) => a - b);
26 }
27 
28 route(key: string): string {
29 if (this.sortedHashes.length === 0) {
30 throw new Error('No shards configured');
31 }
32 
33 const hash = this.hash(key);
34 let idx = this.binarySearch(hash);
35 if (idx >= this.sortedHashes.length) {
36 idx = 0;
37 }
38 return this.ring.get(this.sortedHashes[idx])!;
39 }
40 
41 private hash(key: string): number {
42 const digest = createHash('md5').update(key).digest();
43 return digest.readUInt32BE(0);
44 }
45 
46 private binarySearch(target: number): number {
47 let lo = 0;
48 let hi = this.sortedHashes.length;
49 while (lo < hi) {
50 const mid = (lo + hi) >>> 1;
51 if (this.sortedHashes[mid] < target) lo = mid + 1;
52 else hi = mid;
53 }
54 return lo;
55 }
56}
57 

Connection Pool Manager

typescript
1import { Pool, PoolConfig, QueryResult } from 'pg';
2 
3class PoolManager {
4 private pools: Map<string, Pool> = new Map();
5 
6 async initialize(configs: ShardConfig[]): Promise<void> {
7 for (const config of configs) {
8 const poolConfig: PoolConfig = {
9 host: config.host,
10 port: config.port,
11 database: config.database,
12 user: config.user,
13 password: config.password,
14 max: config.maxConnections,
15 idleTimeoutMillis: 30000,
16 connectionTimeoutMillis: 5000,
17 ssl: config.ssl ? { rejectUnauthorized: false } : undefined,
18 };
19 
20 const pool = new Pool(poolConfig);
21 pool.on('error', (err) => {
22 console.error(`Pool error on shard ${config.id}:`, err.message);
23 });
24 
25 // Verify connectivity
26 const client = await pool.connect();
27 await client.query('SELECT 1');
28 client.release();
29 
30 this.pools.set(config.id, pool);
31 }
32 }
33 
34 getPool(shardId: string): Pool {
35 const pool = this.pools.get(shardId);
36 if (!pool) throw new Error(`Unknown shard: ${shardId}`);
37 return pool;
38 }
39 
40 getAllShardIds(): string[] {
41 return Array.from(this.pools.keys());
42 }
43 
44 async close(): Promise<void> {
45 await Promise.all(
46 Array.from(this.pools.values()).map((pool) => pool.end())
47 );
48 }
49 
50 getStats(): Record<string, PoolStats> {
51 const stats: Record<string, PoolStats> = {};
52 for (const [id, pool] of this.pools) {
53 stats[id] = {
54 totalCount: pool.totalCount,
55 idleCount: pool.idleCount,
56 waitingCount: pool.waitingCount,
57 };
58 }
59 return stats;
60 }
61}
62 
63interface PoolStats {
64 totalCount: number;
65 idleCount: number;
66 waitingCount: number;
67}
68 

Sharded Database Client

typescript
1class ShardedDB {
2 constructor(
3 private router: ConsistentHashRouter,
4 private pools: PoolManager
5 ) {}
6 
7 async query<T = any>(
8 shardKey: string,
9 sql: string,
10 params?: any[]
11 ): Promise<T[]> {
12 const shardId = this.router.route(shardKey);
13 const pool = this.pools.getPool(shardId);
14 const result = await pool.query(sql, params);
15 return result.rows as T[];
16 }
17 
18 async queryOne<T = any>(
19 shardKey: string,
20 sql: string,
21 params?: any[]
22 ): Promise<T | null> {
23 const rows = await this.query<T>(shardKey, sql, params);
24 return rows[0] ?? null;
25 }
26 
27 async execute(
28 shardKey: string,
29 sql: string,
30 params?: any[]
31 ): Promise<number> {
32 const shardId = this.router.route(shardKey);
33 const pool = this.pools.getPool(shardId);
34 const result = await pool.query(sql, params);
35 return result.rowCount ?? 0;
36 }
37 
38 async transaction<T>(
39 shardKey: string,
40 fn: (client: PoolClient) => Promise<T>
41 ): Promise<T> {
42 const shardId = this.router.route(shardKey);
43 const pool = this.pools.getPool(shardId);
44 const client = await pool.connect();
45 
46 try {
47 await client.query('BEGIN');
48 const result = await fn(client);
49 await client.query('COMMIT');
50 return result;
51 } catch (error) {
52 await client.query('ROLLBACK');
53 throw error;
54 } finally {
55 client.release();
56 }
57 }
58 
59 async scatterGather<T = any>(
60 sql: string,
61 params?: any[],
62 options: { timeout?: number; allowPartialFailure?: boolean } = {}
63 ): Promise<ScatterResult<T>[]> {
64 const { timeout = 10000, allowPartialFailure = true } = options;
65 const shardIds = this.pools.getAllShardIds();
66 
67 const promises = shardIds.map(async (shardId): Promise<ScatterResult<T>> => {
68 try {
69 const pool = this.pools.getPool(shardId);
70 const result = await Promise.race([
71 pool.query(sql, params),
72 new Promise<never>((_, reject) =>
73 setTimeout(() => reject(new Error('Query timeout')), timeout)
74 ),
75 ]);
76 return { shardId, data: result.rows as T[] };
77 } catch (error) {
78 if (!allowPartialFailure) throw error;
79 return { shardId, data: [], error: error as Error };
80 }
81 });
82 
83 return Promise.all(promises);
84 }
85 
86 async aggregate<T, R>(
87 sql: string,
88 params: any[] | undefined,
89 reducer: (results: ScatterResult<T>[]) => R
90 ): Promise<R> {
91 const results = await this.scatterGather<T>(sql, params);
92 return reducer(results);
93 }
94}
95 

Need a second opinion on your system design architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Type-Safe Repository

typescript
1interface Order {
2 tenantId: string;
3 orderId: string;
4 customerId: string;
5 status: string;
6 totalCents: number;
7 currency: string;
8 createdAt: Date;
9}
10 
11interface OrderRow {
12 tenant_id: string;
13 order_id: string;
14 customer_id: string;
15 status: string;
16 total_cents: string; // pg returns bigint as string
17 currency: string;
18 created_at: Date;
19}
20 
21class OrderRepository {
22 constructor(private db: ShardedDB) {}
23 
24 async create(order: Order): Promise<void> {
25 await this.db.execute(
26 order.tenantId,
27 `INSERT INTO orders (tenant_id, order_id, customer_id, status, total_cents, currency, created_at)
28 VALUES ($1, $2, $3, $4, $5, $6, $7)`,
29 [order.tenantId, order.orderId, order.customerId,
30 order.status, order.totalCents, order.currency, order.createdAt]
31 );
32 }
33 
34 async findById(tenantId: string, orderId: string): Promise<Order | null> {
35 const row = await this.db.queryOne<OrderRow>(
36 tenantId,
37 `SELECT tenant_id, order_id, customer_id, status, total_cents, currency, created_at
38 FROM orders WHERE tenant_id = $1 AND order_id = $2`,
39 [tenantId, orderId]
40 );
41 return row ? this.toOrder(row) : null;
42 }
43 
44 async listByTenant(tenantId: string, limit = 20, offset = 0): Promise<Order[]> {
45 const rows = await this.db.query<OrderRow>(
46 tenantId,
47 `SELECT tenant_id, order_id, customer_id, status, total_cents, currency, created_at
48 FROM orders WHERE tenant_id = $1
49 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
50 [tenantId, limit, offset]
51 );
52 return rows.map(this.toOrder);
53 }
54 
55 async getGlobalStats(): Promise<{ totalOrders: number; totalRevenue: number }> {
56 return this.db.aggregate<{ count: string; revenue: string }, { totalOrders: number; totalRevenue: number }>(
57 `SELECT COUNT(*) as count, COALESCE(SUM(total_cents), 0) as revenue
58 FROM orders WHERE created_at > NOW() - INTERVAL '30 days'`,
59 undefined,
60 (results) => {
61 let totalOrders = 0;
62 let totalRevenue = 0;
63 for (const result of results) {
64 for (const row of result.data) {
65 totalOrders += parseInt(row.count, 10);
66 totalRevenue += parseInt(row.revenue, 10);
67 }
68 }
69 return { totalOrders, totalRevenue };
70 }
71 );
72 }
73 
74 async createInTransaction(tenantId: string, order: Order, items: OrderItem[]): Promise<void> {
75 await this.db.transaction(order.tenantId, async (client) => {
76 await client.query(
77 `INSERT INTO orders (tenant_id, order_id, customer_id, status, total_cents, currency, created_at)
78 VALUES ($1, $2, $3, $4, $5, $6, $7)`,
79 [order.tenantId, order.orderId, order.customerId,
80 order.status, order.totalCents, order.currency, order.createdAt]
81 );
82 
83 for (const item of items) {
84 await client.query(
85 `INSERT INTO order_items (tenant_id, order_id, item_id, product_id, quantity, unit_price)
86 VALUES ($1, $2, $3, $4, $5, $6)`,
87 [tenantId, order.orderId, item.itemId, item.productId, item.quantity, item.unitPrice]
88 );
89 }
90 });
91 }
92 
93 private toOrder(row: OrderRow): Order {
94 return {
95 tenantId: row.tenant_id,
96 orderId: row.order_id,
97 customerId: row.customer_id,
98 status: row.status,
99 totalCents: parseInt(row.total_cents, 10),
100 currency: row.currency,
101 createdAt: row.created_at,
102 };
103 }
104}
105 
106interface OrderItem {
107 itemId: string;
108 productId: string;
109 quantity: number;
110 unitPrice: number;
111}
112 

Express/Fastify Integration

typescript
1import Fastify from 'fastify';
2import { randomUUID } from 'crypto';
3 
4const app = Fastify();
5 
6let shardedDB: ShardedDB;
7 
8app.addHook('onReady', async () => {
9 const configs = loadShardConfigs();
10 const router = new ConsistentHashRouter();
11 const poolManager = new PoolManager();
12 
13 for (const config of configs) {
14 router.addShard(config.id);
15 }
16 await poolManager.initialize(configs);
17 
18 shardedDB = new ShardedDB(router, poolManager);
19});
20 
21app.post<{ Body: { tenantId: string; customerId: string; totalCents: number; currency: string } }>(
22 '/orders',
23 async (request, reply) => {
24 const repo = new OrderRepository(shardedDB);
25 const order: Order = {
26 tenantId: request.body.tenantId,
27 orderId: randomUUID(),
28 customerId: request.body.customerId,
29 status: 'placed',
30 totalCents: request.body.totalCents,
31 currency: request.body.currency,
32 createdAt: new Date(),
33 };
34 
35 await repo.create(order);
36 reply.status(201).send({ orderId: order.orderId });
37 }
38);
39 
40app.get<{ Params: { tenantId: string; orderId: string } }>(
41 '/orders/:tenantId/:orderId',
42 async (request) => {
43 const repo = new OrderRepository(shardedDB);
44 const order = await repo.findById(request.params.tenantId, request.params.orderId);
45 if (!order) throw { statusCode: 404, message: 'Order not found' };
46 return order;
47 }
48);
49 
50app.get('/health/shards', async () => {
51 return shardedDB.pools.getStats();
52});
53 

Testing

typescript
1import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2 
3describe('ConsistentHashRouter', () => {
4 it('routes same key to same shard consistently', () => {
5 const router = new ConsistentHashRouter();
6 router.addShard('shard-0');
7 router.addShard('shard-1');
8 
9 const shard1 = router.route('tenant-abc');
10 const shard2 = router.route('tenant-abc');
11 expect(shard1).toBe(shard2);
12 });
13 
14 it('distributes keys roughly evenly', () => {
15 const router = new ConsistentHashRouter();
16 router.addShard('shard-0');
17 router.addShard('shard-1');
18 router.addShard('shard-2');
19 
20 const counts: Record<string, number> = {};
21 for (let i = 0; i < 10000; i++) {
22 const shard = router.route(`tenant-${i}`);
23 counts[shard] = (counts[shard] ?? 0) + 1;
24 }
25 
26 for (const [shard, count] of Object.entries(counts)) {
27 const ratio = count / 10000;
28 expect(ratio).toBeGreaterThan(0.15);
29 expect(ratio).toBeLessThan(0.55);
30 }
31 });
32 
33 it('minimizes key movement when adding a shard', () => {
34 const router = new ConsistentHashRouter();
35 router.addShard('shard-0');
36 router.addShard('shard-1');
37 
38 const initial = new Map<string, string>();
39 for (let i = 0; i < 1000; i++) {
40 initial.set(`key-${i}`, router.route(`key-${i}`));
41 }
42 
43 router.addShard('shard-2');
44 
45 let moved = 0;
46 for (const [key, originalShard] of initial) {
47 if (router.route(key) !== originalShard) moved++;
48 }
49 
50 expect(moved / 1000).toBeLessThan(0.5);
51 });
52});
53 

Conclusion

TypeScript's type system adds a valuable safety layer to database sharding infrastructure. Generic type parameters ensure scatter-gather results maintain their types through aggregation, the repository pattern provides clean separation between sharding mechanics and business logic, and async/await produces readable scatter-gather code that handles partial failures gracefully. Combined with the pg library's connection pooling, TypeScript provides a production-ready sharding foundation for Node.js applications.

FAQ

Need expert help?

Building with system design?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026