In the previous chapter, we learned replication.
Replication means:
Keep multiple copies of the same data.
Sharding is different.
Sharding means:
Split data across multiple database nodes so that each node stores only part of the total dataset.
For example, suppose we have 1 billion users.
A single database might become difficult to scale:
1 Billion Users
|
v
+-----------+
| Database |
+-----------+With sharding:
1 Billion Users
|
+------------+------------+
| | |
v v v
Shard 1 Shard 2 Shard 3
Users Users Users
1-333M 334-666M 667-1BEach shard stores only a portion of the data.
This can allow the system to scale beyond the limits of one database server.
These concepts are often confused.
Copies the same data.
Primary
|
+---- Replica 1
|
+---- Replica 2All replicas contain approximately the same dataset.
Splits data into partitions.
Database
|
+---- Partition A
+---- Partition B
+---- Partition CPartitions may exist inside one database system.
Distributes partitions across different database nodes.
Database Cluster
|
+----------+----------+
| | |
v v v
Shard 1 Shard 2 Shard 3A useful mental model:
Replication = Copy
Partitioning = Split
Sharding = Split across nodesImagine an application with:
Users = 500 million
Orders = 10 billion
Writes = 200,000/sec
Reads = 2 million/secA single database may encounter limits involving:
Adding read replicas can help with reads.
But consider:
200k writes/sec
|
v
PrimaryThe primary remains responsible for writes.
Replication does not automatically distribute write ownership.
Sharding can distribute the dataset and write workload.
Write Traffic
|
+-------+-------+
| | |
v v v
Shard 1 Shard 2 Shard 3Before discussing sharding, understand vertical partitioning.
Vertical partitioning means splitting columns.
Suppose we have:
Users
------------------------------------
id
name
email
password_hash
profile
preferences
large_bio
avatar_metadataWe might split it into:
UserCore
----------------
id
name
email
password_hashand:
UserProfile
----------------
user_id
profile
preferences
large_bio
avatar_metadataThis can reduce row size and separate workloads.
Horizontal partitioning splits rows.
Suppose:
Users
1
2
3
4
5
6
7
8
9
10We can divide them:
Partition A
1
2
3
4
5
Partition B
6
7
8
9
10The columns remain the same.
Only the rows are divided.
This is the foundation of many sharding strategies.
| Type | Splits | Example |
|---|---|---|
| Vertical | Columns | Profile data separated from authentication data |
| Horizontal | Rows | Users 1–1M in one partition, others elsewhere |
Horizontal partitioning is particularly important for large-scale databases.
The shard key determines which shard stores a record.
Suppose:
user_id = 12345The system calculates:
shard = hash(user_id) % number_of_shardsExample:
hash(12345) % 4 = 2Therefore:
user 12345 -> Shard 2The shard key is one of the most important design decisions in a sharded system.
A good shard key should ideally provide:
Avoid sending most data to one shard.
Writes should be reasonably balanced.
Common queries should be able to identify the relevant shard.
There should be many possible values.
Changing the shard key should not be required frequently.
Suppose we shard users by:
countryPossible distribution:
India 600M
USA 200M
Europe 150M
Others 50MNow:
Shard India = 600M
Shard USA = 200M
Shard Europe= 150M
Shard Others= 50MThe distribution is highly uneven.
India becomes a hot shard.
This is called data skew.
Consider:
countryLow-to-moderate cardinality.
Possible values:
India
USA
UK
Germany
France
...Now consider:
user_idPotentially hundreds of millions of values.
That gives much higher cardinality.
High cardinality often makes a better shard key, although the correct choice always depends on query patterns and workload.
A shard key should not be selected only based on even distribution.
Suppose most queries are:
Get all orders for user 123Then:
user_idcan be a strong shard key.
The system can route the query directly:
user_id = 123
|
v
Shard calculation
|
v
Shard 7Instead of querying every shard.
Suppose the shard key is user_id.
Now someone asks:
SELECT COUNT(*)
FROM orders
WHERE status = 'PAID';There may be no user ID.
The router may need to query every shard:
Query
|
+--------+--------+
| | |
v v v
Shard 1 Shard 2 Shard 3
| | |
v v v
Count Count Count
\ | /
\ | /
+------+-----+
|
Combine
|
v
ResultThis is called a scatter-gather query.
It can be expensive.
Suppose there are 100 shards.
A query that needs all shards becomes:
1 request
|
+----> 100 shard requestsThen the system must:
Latency may be dominated by the slowest shard.
Therefore:
Good shard-key design attempts to keep common queries shard-local.
A common approach is hashing the shard key.
Example:
shard = hash(user_id) % NIf:
N = 4then:
user 101 -> hash -> 1
user 102 -> hash -> 3
user 103 -> hash -> 0
user 104 -> hash -> 2The goal is to distribute records evenly.
Hash sharding can provide:
Example:
User IDs
|
Hash
|
+-----+-----+
| | |
v v v
S1 S2 S3Hash sharding can make range queries difficult.
Suppose we want:
Users 1,000,000 through 2,000,000With hashing, those users may be spread across many shards.
Therefore:
Range Query
|
+----> S1
+----> S2
+----> S3
+----> S4This can become a scatter-gather query.
Instead of hashing, divide the key space into ranges.
Example:
Shard 1:
user_id 1 - 1,000,000
Shard 2:
user_id 1,000,001 - 2,000,000
Shard 3:
user_id 2,000,001 - 3,000,000Architecture:
User ID
|
+------+------+
| |
Range Range
| |
v v
Shard 1 Shard 2Range sharding is useful when queries frequently use ranges.
Example:
WHERE created_at BETWEEN
'2026-01-01' AND '2026-01-31'A time-based range can make it easier to locate the relevant partition.
Range sharding can create hotspots.
Suppose new records always have increasing IDs:
1
2
3
4
...
999999
1000000
1000001New writes may all target the latest range.
Old shards:
mostly idle
Latest shard:
########################
heavy writesThis is a hot partition.
A hot partition is a partition receiving disproportionately high traffic.
Example:
Shard 1 10k writes/sec
Shard 2 11k writes/sec
Shard 3 9k writes/sec
Shard 4 500k writes/sec <-- HOTThe entire cluster can be healthy while one shard becomes the bottleneck.
This is why average cluster CPU can be misleading.
Another strategy is to maintain a mapping:
user_id -> shardExample:
User 101 -> Shard A
User 102 -> Shard C
User 103 -> Shard BA routing service or metadata store maintains this information.
Architecture:
Application
|
v
Shard Directory
|
+----> Shard A
+----> Shard B
+----> Shard CIt provides flexibility.
A user can move:
User 101
Shard A
|
| migration
v
Shard CThe directory is updated:
101 -> CThe application does not need to know the physical placement.
The directory becomes important infrastructure.
Potential issues:
The directory itself may need replication and high availability.
Normal modulo hashing has a major problem.
Suppose:
shard = hash(key) % 4Now add a fifth shard:
shard = hash(key) % 5Many keys map to different shards.
This can cause massive data movement.
Consistent hashing reduces the amount of data that must move when nodes are added or removed.
Imagine a circular hash space:
0
.-------.
. .
3 1
. .
'-------'
2Nodes are placed on the ring.
A key is hashed onto the ring and assigned to the next node according to the chosen algorithm.
Conceptually:
Key
|
v
Hash
|
v
Ring
|
v
ShardA shard can be represented by multiple positions on a consistent hash ring.
Shard A -> A1, A2, A3
Shard B -> B1, B2, B3
Shard C -> C1, C2, C3This helps improve distribution.
Virtual nodes can also make rebalancing more manageable.
As the system grows, you may need more shards.
Initially:
S1
S2
S3Later:
S1
S2
S3
S4
S5
S6Data must be redistributed.
This is called rebalancing or resharding.
Moving data while the application is running introduces challenges:
For a billion-row dataset, moving everything at once is not practical.
A production system may migrate data gradually.
Conceptually:
Old Shard
|
| copy data
v
New ShardDuring migration:
Application
|
v
Routing Layer
|
+---- old location
|
+---- new locationThe system keeps the old and new locations synchronized until the migration is safe.
Then routing switches to the new shard.
During migration, an application may temporarily write to both locations:
Application
|
+----> Old Shard
|
+----> New ShardThis is called a dual-write strategy.
But dual writes introduce difficult failure scenarios.
For example:
Write Old = SUCCESS
Write New = FAILURENow the two copies differ.
Therefore dual writes require careful reconciliation and validation.
Another strategy is to:
Conceptually:
Old Shard
|
+---- Initial Copy ----> New Shard
|
+---- Change Stream ---> New ShardThis allows migration while the source remains active.
Large systems need metadata describing shard placement.
Example:
Shard Map
Range / Key Shard
-------------------------
0 - 999999 S1
1000000 - 1999999 S2
2000000 - 2999999 S3The routing layer uses this map to find the correct shard.
The metadata itself becomes a critical system component.
A shard router determines where a request should go.
Application
|
v
Shard Router
|
+----------+----------+
| | |
v v v
S1 S2 S3Example:
const shard = getShard(userId);
return shard.query(...);The real implementation may use:
For SaaS systems, a common strategy is:
tenant_id -> shardExample:
Tenant A -> Shard 1
Tenant B -> Shard 2
Tenant C -> Shard 1
Tenant D -> Shard 3Advantages:
But a very large tenant can become a hot shard.
Suppose:
Tenant A = 1 million requests/sec
Tenant B = 10k requests/sec
Tenant C = 5k requests/secIf tenant ID determines the shard:
Tenant A -> Shard 1Shard 1 becomes overloaded.
Possible solutions include:
This illustrates why a shard key must account for workload distribution, not just data distribution.
Sometimes one field is insufficient.
A system may use:
tenant_id + user_idor:
tenant_id + hash(user_id)This can preserve tenant locality while distributing users within a large tenant.
Example:
Tenant A
|
+---- hash(user 1) -> S1
+---- hash(user 2) -> S3
+---- hash(user 3) -> S2This is more flexible but increases routing complexity.
Suppose the most common query is:
Get orders for user_idThen:
shard key = user_idmay be useful.
Suppose the most common query is:
Get all orders for tenant_idThen:
shard key = tenant_idmay provide better locality.
The correct key depends on the application's access patterns.
Transactions become more complicated when data lives on different shards.
Example:
User data -> Shard 1
Order data -> Shard 2
Payment data -> Shard 3Now suppose one business operation updates all three.
Transaction
|
+---- S1
+---- S2
+---- S3A traditional single-database transaction cannot automatically provide the same semantics across independently managed shards.
Distributed transaction mechanisms may be required.
They can introduce:
A common system-design strategy is to design data ownership so a business operation stays within one shard.
For example:
user_id
|
v
Shard 7
User
Orders
Cart
PreferencesIf most operations are user-centric, placing related data together can reduce cross-shard operations.
This is sometimes called co-location.
Suppose:
Users
Orders
Cartall use:
user_idas a routing key.
Then:
User 123
|
+---- Users -> S4
+---- Orders -> S4
+---- Cart -> S4A request involving all three can remain on the same shard.
This can significantly simplify architecture.
Suppose:
SELECT *
FROM users
JOIN orders ON users.id = orders.user_id;If users and orders are distributed differently, the database may need to perform a distributed join.
That can be expensive.
A common solution is to:
Each shard typically maintains indexes for the data it owns.
Shard 1
|
+-- Data
+-- Indexes
Shard 2
|
+-- Data
+-- IndexesThis can make local indexes smaller than one giant global index.
But global uniqueness can become harder.
Suppose email addresses must be globally unique.
With one database:
UNIQUE(email)is straightforward.
With many shards:
S1 -> users
S2 -> users
S3 -> usersA unique email could theoretically exist on two shards.
Possible approaches:
Each adds complexity.
Auto-increment IDs can become complicated.
Suppose:
Shard 1 -> ID 1
Shard 2 -> ID 1Now IDs collide globally.
Possible strategies:
The choice depends on ordering, size, indexing, and operational requirements.
A shard can also become hot because one key receives extreme traffic.
Example:
celebrity_user_id = 123
Requests:
10 million/secIf all requests route to one shard:
Shard 7
###########
###########
###########The shard becomes a bottleneck.
Solutions can include:
Sharding alone does not solve hot-key problems.
Suppose:
User 123 profileis requested millions of times.
Instead of querying the shard every time:
Clients
|
v
Cache
|
cache hit
|
v
ResponseOnly cache misses reach the shard.
This reduces database pressure.
A common production architecture combines both.
Shard Router
|
+-------------+-------------+
| | |
v v v
Shard A Shard B Shard C
/ \ / \ / \
v v v v v v
R-A1 R-A2 R-B1 R-B2 R-C1 R-C2Each shard may have:
Therefore:
Sharding = distribute data
Replication = copy each shardThis combination is extremely common in large distributed systems.
Suppose:
Shard B Primary
XIts replica can potentially be promoted:
Shard B Replica
|
v
New Shard B PrimaryOther shards continue operating:
Shard A -> healthy
Shard B -> failover
Shard C -> healthyThe failure of one shard should ideally not bring down the entire cluster.
This is another reason for isolating failure domains.
Suppose one shard safely handles:
20,000 writes/secand we need:
100,000 writes/secA simple starting estimate:
100,000 / 20,000 = 5At least five shards are required under that simplified assumption.
But production systems need headroom.
If we target 60% utilization:
Required capacity =
100,000 / 0.60
= 166,667 writes/secThen:
166,667 / 20,000
≈ 8.34So approximately nine shards may be needed under these assumptions.
Capacity planning must also consider:
Too few shards:
S1
S2May cause:
Too many shards:
S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 ...May cause:
The goal is not maximum shard count.
The goal is appropriate distribution with sufficient headroom.
Suppose:
100 application instancesand:
50 shardsIf every application instance opens:
20 connections/shardthen:
100 × 50 × 20
= 100,000 database connectionsThat can be disastrous.
Therefore large sharded systems need careful connection management.
Solutions may include:
Useful when:
Useful when:
But watch for hot ranges.
Useful when:
Useful when:
Suppose:
100 million new URLs/dayA URL shortener stores:
short_code
long_url
created_at
user_idA possible shard key:
short_codeFor a lookup:
GET /abc123the router can calculate:
hash("abc123")
|
v
Shard 7Then:
Shard 7
|
+---- abc123 -> https://example.com/...This makes the read path shard-local.
Client
|
v
Load Balancer
|
v
API Servers
|
v
Shard Router
|
+--------------+--------------+
| | |
v v v
Shard 1 Shard 2 Shard 3
/ \ / \ / \
R1 R2 R1 R2 R1 R2Reads can be distributed among replicas within the selected shard.
Writes go to the primary of the selected shard.
Suppose a social network has:
Users
Posts
Followers
Likes
CommentsA possible approach is:
user_id -> shardThen user-centric data can be co-located.
User 123 -> Shard 4
Posts -> Shard 4
Profile -> Shard 4
Settings -> Shard 4However, a celebrity with millions of followers can create hot-key problems.
The architecture may need:
Sharding is one component of the overall design.
Suppose we store:
IoT sensor readingswith:
device_id
timestamp
valueA range/time-based strategy might partition data by:
day
week
monthExample:
2026-01 -> Partition 1
2026-02 -> Partition 2
2026-03 -> Partition 3This can make time-range queries efficient.
But if all writes target the current time partition, that partition can become hot.
A secondary distribution strategy may be needed.
If one well-designed database can handle the workload, sharding may add unnecessary complexity.
High cardinality does not guarantee good query locality.
Average load can hide one overloaded shard.
A system can become slow if common queries require every shard.
Data grows.
Your architecture must support future expansion.
Poor shard-key design can simply move the bottleneck to one shard.
Sharded systems still need high availability.
Unique constraints become harder across shards.
Cross-shard business operations can be expensive.
Application × shard × pool size can explode.
You have 10 shards. One shard is receiving 40% of all traffic while the other nine share the remaining 60%. What could be wrong?
Possible causes:
1. Poor shard key
2. Data skew
3. Hot key
4. Hot tenant
5. Uneven traffic distribution
6. Range hotspot
7. Application routing bugInvestigation:
Traffic
|
v
Shard distribution
|
+---- requests/shard
+---- storage/shard
+---- CPU/shard
+---- hot keys
+---- query patternsPotential solutions:
Remember sharding with this diagram:
APPLICATION
|
v
SHARD ROUTER
|
+-----------------+-----------------+
| | |
v v v
SHARD A SHARD B SHARD C
/ \ / \ / \
v v v v v v
Primary Replica Primary Replica Primary ReplicaThink in this order:
1. What is the data?
2. How is it accessed?
3. What should determine its shard?
4. Will data be evenly distributed?
5. Will traffic be evenly distributed?
6. Can common queries stay on one shard?
7. How will shards be rebalanced?
8. What happens when a shard fails?
9. How do we replicate each shard?
10. How will the system grow?The most important combined mental model from Chapters 13 and 14 is:
DATASET
|
+------+------+
| |
SHARD SHARD
| |
+----+----+ +----+----+
| | | |
Primary Replica Primary ReplicaReplication answers:
How do we keep multiple copies of each shard?
Sharding answers:
How do we distribute the total dataset across nodes?
Together:
Sharding
+
Replication
=
Scalable + Highly Available Database ArchitectureBut this still does not solve every problem.
You may also need:
In this chapter, we learned:
The central idea is:
Replication copies data to improve availability and read scaling. Sharding splits data to distribute storage and workload. Large-scale systems often combine both.
Before moving forward, make sure you can explain:
Next we move to one of the most important performance and scalability topics in system design: caching.
We will learn:
Mark it as complete to track your mastery.