Sharding (horizontal partitioning) splits one large dataset across several databases, called shards, so that each holds only part of the data. User 1–1M live on shard A, users 1M–2M on shard B, and so on. Each shard handles only its slice of reads and writes, so total capacity grows with the number of shards.
A library too big for one building splits its books across branches: A–F in one, G–M in the next. Each branch serves only its share of readers. It works well until someone asks for "all books published in 2020", which means visiting every branch.
Caches and read replicas scale reads, but every write still lands on one primary, and the whole dataset must fit on it. When write throughput or data size outgrows the biggest practical machine, the only way forward is to divide the data.
shard key = user_id
app --> router --+-- hash(user_id) % 4 == 0 --> shard 0 (+ replicas)
+-- ... == 1 ---------------> shard 1
+-- ... == 2 ---------------> shard 2
+-- ... == 3 ---------------> shard 3
Strategies
range: user_id 1-1M -> A, 1M-2M -> B range scans easy; hot ranges likely
hash: hash(key) mod N / consistent hash even spread; range queries scatter
directory: a lookup table key -> shard flexible moves; the table is a dependency
geo: EU users -> EU shard data residency, latency
The shard key is the most important decision. A good key: (1) has high cardinality, (2) spreads load evenly, and (3) matches how data is queried so most requests touch one shard. For a chat app, conversation_id keeps each conversation's messages together. For a multi-tenant SaaS, tenant_id keeps each customer on one shard (with care for very large tenants).
hash % N, changing N moves almost everything; consistent hashing or many small virtual shards mapped to few machines moves only a fraction.