Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Sep 27, 2026·19 min read

Scaling Data: Partitioning, Sharding & Replication Strategies

Scaling data is a consistency and coordination problem, not a storage problem. This article covers table partitioning, sharding strategies and their irreversibility, and the write/read consistency tradeoffs of Master-Slave, Multi-Master, and Leaderless replication — including the quorum formula that makes leaderless replication safe.

Scaling Data: Partitioning, Sharding & Replication Strategies

A database is not a dumb storage box — it is a contract between your write path, your read path, and your consistency guarantees. Every design decision you defer becomes a production incident you eventually own. The most expensive deferred decision in data architecture is sharding. Engineers reach for it when writes slow down — then discover it is irreversible, that cross-shard joins are now application code, and that cross-shard transactions are now Sagas. This article maps each scaling strategy to the consistency tradeoff it makes so you choose before you commit, not after.

Architectural Note

This is Part 3 of the Advanced Database & State Management series. It builds directly on the consistency model vocabulary from Part 2b — Consistency Models & Distributed Coordination. Replication topology choices are meaningless without understanding what "consistent" means across replicas.

Architectural Note

Cross-series reference: The Polyglot Persistence: When Multiple Databases Earn Their Cost article in the Modern Database Paradigms series covers CDC (Change Data Capture) via Debezium as the correct alternative to dual-write across multiple databases. If your sharding strategy requires cross-shard writes, Debezium-based CDC from a single source of truth is the pattern to use — not application-level dual-write.


1. Recognizing the Vertical Ceiling Before You Hit It

Before reaching for partitioning or sharding, verify you have actually hit the vertical scaling ceiling — not just the application layer's interpretation of it.

1.1 Signals That Precede the Actual Ceiling

Signal What It Means First Fix
p99 write latency rising Lock contention, index bloat, or buffer pool pressure EXPLAIN ANALYZE, vacuum, index audit
Lock contention rate high Too many writers competing for the same rows Optimistic concurrency, smaller transactions
autovacuum perpetually lagging Dead tuple bloat — MVCC accumulation exceeds vacuum throughput Tune autovacuum_vacuum_cost_delay, dedicate a vacuum worker
Connection pool exhaustion Too many application threads holding idle connections PgBouncer transaction-mode pooling before sharding
Checkpoint duration > checkpoint_completion_target WAL flush can't keep up with write rate Increase max_wal_size, tune checkpoint_completion_target
SQL
-- ❌ Engineers shard when they see this
SELECT count(*) FROM orders;  -- 50M rows, queries feel slow

-- ✅ First check: is the query actually slow, or is an index missing?
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 12345;
-- If Seq Scan with 50M rows: add index
-- If Index Scan but still slow: look at buffer hits/misses in BUFFERS output

-- Then check connection count before sharding
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
-- If this number is high: PgBouncer first, not sharding
Pro Tip & Optimization

PgBouncer before sharding: Postgres struggles with connection count above ~500 active connections per node. PgBouncer in transaction mode multiplexes thousands of application threads onto a small pool of actual Postgres connections — eliminating a common "database is slow" symptom that has nothing to do with data volume.


2. Table Partitioning: Optimization, Not Scaling

Table partitioning divides a single logical table into multiple physical subtables (partitions). It is a query-optimization strategy — it does not distribute data across machines.

2.1 Partition Types

SQL
-- Range partitioning: correct for time-series data, log tables, audit trails
CREATE TABLE orders (
  id         BIGSERIAL,
  created_at TIMESTAMPTZ NOT NULL,
  status     TEXT,
  PRIMARY KEY (id, created_at)  -- partition key must be in PK
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2024 PARTITION OF orders
  FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE orders_2025 PARTITION OF orders
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

-- ✅ Queries with a created_at filter only scan the matching partition
-- (partition pruning — Postgres eliminates irrelevant partitions from the plan)
EXPLAIN SELECT * FROM orders WHERE created_at >= '2025-06-01';
-- → only scans orders_2025

-- ✅ Old partitions can be detached and archived without locking the entire table
ALTER TABLE orders DETACH PARTITION orders_2024;
SQL
-- Hash partitioning: correct when you want even write distribution across partitions
-- (no natural range key, or avoid hot spots in a range partition)
CREATE TABLE sessions (
  id      UUID NOT NULL,
  user_id BIGINT,
  data    JSONB
) PARTITION BY HASH (id);

CREATE TABLE sessions_p0 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions FOR VALUES WITH (MODULUS 4, REMAINDER 3);

2.2 What Partitioning Removes

SQL
-- ❌ These do NOT work across partitions:

-- 1. Unique constraints not including the partition key
CREATE UNIQUE INDEX ON orders(id);  -- ✗ Fails if id is not the partition key

-- 2. Foreign keys referencing partitioned tables from non-partitioned tables
-- (works FROM partitioned TO non-partitioned, not the reverse in older Postgres)

-- 3. Full-table exclusive locks (e.g., ALTER TABLE) are per-partition in Pg 12+
--    but CLUSTER and some operations still lock all partitions

-- ✅ Use partitioning for:
--   - Time-windowed data with range queries on the partition key
--   - Tables where you want to drop/archive whole time windows cheaply
--   - Reducing index size per partition (smaller B-Trees = faster scans)
Crucial Requirement

Partitioning and sharding are orthogonal strategies. Table partitioning is single-node. Sharding is multi-node. You can partition a table on a sharded node. Partition first to optimize queries; shard only when you exhaust the vertical limit of a single node.


3. Sharding: The Irreversible Decision

Sharding distributes data across multiple independent database nodes (shards), each owning a subset of the data. It distributes write load across nodes — something partitioning cannot do.

3.1 Sharding Strategies

Range-Based Sharding

Shard A: customer_id 1 – 1,000,000
Shard B: customer_id 1,000,001 – 2,000,000
Shard C: customer_id 2,000,001 – 3,000,000

Advantages: Predictable placement, easy range queries within a shard. Disadvantage: Hot spots. If most active customers are in the 1M–2M range, Shard B gets all the write traffic. Rebalancing requires moving large contiguous ranges.

Hash-Based Sharding

shard = hash(customer_id) % num_shards

customer_id = 42:    hash(42) % 4 = 2  → Shard C
customer_id = 43:    hash(43) % 4 = 3  → Shard D
customer_id = 44:    hash(44) % 4 = 0  → Shard A

Advantages: Even write distribution, no hot spots. Disadvantage: Range queries across the shard key require hitting all shards. Re-sharding (adding shards) invalidates all hash assignments — requires full data rehash or consistent hashing (minimizes remapping on node addition).

Directory-Based Sharding (Consistent Hashing)

Shard routing table (maintained by a router service):
  customer_id 42   → Shard C
  customer_id 1001 → Shard A
  customer_id 8832 → Shard B
  ...

-- Router service maps each key to a shard dynamically
-- Adding a new shard: only remaps a fraction of keys (consistent hashing ring)

Advantages: Most flexible, supports heterogeneous shards, minimizes remapping on rebalance. Disadvantage: Router service is a single point of failure; routing table must be highly available.

3.2 The Operational Contract You Sign

SQL
-- ❌ Before sharding: foreign keys, joins, transactions work normally
SELECT o.*, c.name, c.email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'pending';

-- ❌ After sharding on customer_id: order and customer may be on different shards
-- The JOIN above becomes:
-- 1. Query Shard A for pending orders: [order_id=1, customer_id=44]
-- 2. Query Shard C (where customer_id=44 lives) for customer name
-- 3. Application joins the two result sets in memory

-- ✅ Design for co-location: shard both orders AND customers on customer_id
-- Then: orders for customer 44 AND customer 44's profile both live on Shard C
-- The join can execute within a single shard
Performance / Safety Warning

Sharding is effectively irreversible. Once you shard:

  • Foreign key constraints across shard boundaries disappear — referential integrity is now your application's problem.
  • Cross-shard transactions must be replaced by Saga patterns — 2PC across shards is operationally fragile.
  • Re-sharding (changing shard count) requires dual-write migration or a full rebuild — plan the initial shard count for 3–5× current volume.
  • Every new database feature evaluation must account for sharding compatibility.

4. Replication Topologies

Replication copies data from one node to one or more others. Unlike sharding, replication duplicates data — each node has the same data. This provides read scalability and fault tolerance, not write scalability.

4.1 Master-Slave (Single Primary)

[Primary] ← writes only
  ├── WAL stream →  [Replica 1] ← reads
  ├── WAL stream →  [Replica 2] ← reads
  └── WAL stream →  [Replica 3] ← reads
  • Writes: Primary only. All write capacity is limited to one node.
  • Reads: Distributed across replicas — scales read throughput linearly with replica count.
  • Consistency: Replicas lag behind primary. Reads from replicas may be stale.
  • Failover: Promote a replica to primary. Manual or automatic (Patroni, repmgr).
SQL
-- ✅ Detecting replication lag on a standby
SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;

-- ✅ If lag > your SLA, route reads to primary for that session
-- (or use synchronous_commit = remote_write to bound maximum lag)
SET synchronous_commit = 'remote_write';

4.2 Multi-Master (Active-Active)

[Primary A] ← writes     [Primary B] ← writes
     │                          │
     └──── bidirectional ───────┘
           replication
  • Writes: Any primary accepts writes simultaneously.
  • Conflict: Concurrent writes to the same row on different primaries → conflict.
  • Resolution strategies:
    • Last-Write-Wins (LWW): timestamp determines winner — silently discards the loser.
    • Application-level merge: application receives both versions and merges.
    • CRDTs: automatic conflict-free merge for compatible data types.
SQL
-- ❌ Multi-master without conflict detection
-- Session on Primary A: UPDATE balance = 900 WHERE id = 42
-- Session on Primary B (concurrent): UPDATE balance = 800 WHERE id = 42
-- LWW resolution: the later timestamp wins → 800
-- A's update is gone. Lost update. No error surfaced to the application.
Performance / Safety Warning

Multi-master LWW conflict resolution silently discards writes. This is not a configuration option — it is the logical consequence of accepting concurrent writes to the same row on different nodes with no distributed lock. For any data where concurrent writes have semantic meaning (balances, counters, inventory), you need application-level conflict detection or CRDTs.

4.3 Leaderless Replication (Dynamo-Style)

There is no designated primary. Any node accepts both reads and writes. Consistency is achieved through quorum.

N = 5 nodes
W = 3 (write quorum: write must be acknowledged by 3 nodes before success)
R = 3 (read quorum: read must be confirmed by 3 nodes before returning result)

W + R = 6 > N = 5
∴ At least 1 node must overlap between write quorum and read quorum
∴ Every read will see at least one node that has the latest write
Quorum reads and writes: N=5 node ring with W=3 cyan write quorum and R=3 green read quorum showing guaranteed overlap
Quorum reads and writes: N=5 node ring with W=3 cyan write quorum and R=3 green read quorum showing guaranteed overlap
PYTHON
# Tuning W and R for different SLAs:

# High write availability (tolerate more stale reads):
W = 1  # Writes acknowledged after 1 node — fast, low durability
R = 5  # Reads from all nodes — expensive, always fresh
# W + R = 6 > 5 ✅ — but writes are fragile (1 node failure loses the write)

# High read availability (tolerate write latency):
W = 3  # Writes quorum — durable
R = 1  # Read from any node — fast, may be stale
# W + R = 4 < 5 ✗ — no overlap guarantee → stale reads possible

# Balanced (Cassandra QUORUM):
W = ceil(N/2) + 1 = 3  # Majority write
R = ceil(N/2) + 1 = 3  # Majority read
# W + R = 6 > 5 ✅ — strong consistency guarantee

5. Replication Topology Decision Matrix

Replication topology comparison: Master-Slave vs Multi-Master vs Leaderless across write consistency, read consistency, conflict resolution, and failure recovery
Replication topology comparison: Master-Slave vs Multi-Master vs Leaderless across write consistency, read consistency, conflict resolution, and failure reco…
Topology Write Consistency Read Consistency Conflict Resolution Failure Recovery
Master-Slave Strong (single writer) Eventual (replica lag) None needed (single writer) Promote replica (minutes)
Multi-Master Eventual (concurrent writers) Eventual LWW / app merge / CRDT No failover needed (other primary continues)
Leaderless Tunable (W quorum) Tunable (R quorum) Read repair / anti-entropy No failover (surviving nodes maintain quorum)

6. Logical vs Physical Replication

Postgres offers two replication modes with different use cases:

Dimension Physical Replication (WAL streaming) Logical Replication (row-level events)
What is replicated Raw WAL bytes — byte-for-byte identical copy Row-level INSERT/UPDATE/DELETE events
Cross-version ✗ — same major version only ✅ — different Postgres major versions
Selective tables ✗ — entire cluster ✅ — subscribe to specific tables
CDC pipelines ✗ — not suitable ✅ — feed Debezium/Kafka with row events
Blue-green migration ✗ — full cutover required ✅ — run old and new side-by-side
Replica usage Read-only standby Can write to non-subscribed tables
Pro Tip & Optimization

Use logical replication for zero-downtime Postgres major version upgrades and for feeding CDC pipelines (Debezium → Kafka). Use physical replication for standby high-availability replicas and PITR backup targets. The two can coexist on the same Postgres primary.


Summary

Concept Rule
Vertical ceiling signals Check p99 latency, lock contention, connection pool exhaustion, and vacuum lag before reaching for sharding
PgBouncer first Connection pool exhaustion is a common "database is slow" symptom that PgBouncer solves before sharding is needed
Table partitioning Query optimization on a single node — not distributed scaling; enables cheap time-window archival
Sharding Distributes write load; is irreversible; cross-shard joins become application joins; plan shard count for 3–5× current volume
Co-location Shard related entities on the same key so joins stay within a single shard
Master-Slave Strong single-writer consistency; read replicas lag — route consistency-required reads to primary
Multi-Master LWW Silently discards concurrent writes — never use for financial or user-generated content
Leaderless quorum W + R > N guarantees at least one overlapping node has the latest write
Logical replication Required for cross-version upgrades, CDC pipelines, and blue-green migrations
Physical replication HA standby and PITR backup — byte-identical copy, same Postgres version only

What's Next

Part 5 brings this series to the application layer: ORM Internals — the N+1 problem, why lazy loading fires N SQL queries invisibly, and how the DataLoader pattern is the minimum correct implementation for any list-loading path. → ORM Internals: N+1, Lazy Loading & Eager Loading

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Database Scaling#Sharding#Replication#Distributed Systems
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.