System DesignAdvanced 14 min Lesson 15 of 42

Sharding — Splitting the Data

When one database cannot hold the writes, split the data across several. Learn shard keys, the costs, and why it is a last resort.

System Design · Lesson 15 of 42
0/42 done(0%)

What is it? #

Sharding splits your data across several databases, each holding a portion of the rows.

Replication helps with reads. Sharding is what helps with writes and with data too large for one machine, because each shard handles only its own slice.

Everything depends on the shard key — the column that decides which shard a row belongs to. Choose it well and queries stay fast; choose it badly and you get hot shards and cross-shard queries.

It should be a last resort. Sharding makes joins, transactions, unique constraints and reporting much harder, and undoing it is painful.

Think of it like this #

One library growing too large for its building. You split it across several branches — A–F here, G–M there, and so on.

Each branch is smaller and faster to search. But "how many books do we have in total" now means asking every branch, and moving a book between branches is a real operation rather than shifting a shelf.

Simple example #

A messaging application writes millions of messages a day. One database cannot keep up. Sharding by conversation ID keeps every message of a conversation together, so the common query hits exactly one shard.

Code #

TEXT
Sharding strategies

Range        shard = by id range (1-1M, 1M-2M)
             simple, but new data all lands on the last shard — a hot spot

Hash         shard = hash(key) % shard_count
             even spread, but changing shard_count reshuffles almost everything

Consistent   hash onto a ring
hashing      adding a shard moves only a small fraction of keys

Directory    a lookup table maps key → shard
             flexible, but the lookup becomes a dependency and a bottleneck
PYTHON
import hashlib


class ShardRouter:
    def __init__(self, pools: list):
        self.pools = pools                      # one connection pool per shard

    def shard_for(self, key: str):
        digest = hashlib.sha256(key.encode()).hexdigest()
        return self.pools[int(digest, 16) % len(self.pools)]

    def messages_for(self, conversation_id: str, limit: int = 50):
        pool = self.shard_for(conversation_id)  # one shard only
        with pool.connection() as conn:
            return conn.execute(
                "SELECT * FROM messages WHERE conversation_id = %s "
                "ORDER BY created_at DESC LIMIT %s",
                (conversation_id, limit),
            ).fetchall()

    def total_messages(self) -> int:
        # Cross-shard query: ask every shard and combine. Slow, avoid in hot paths.
        total = 0
        for pool in self.pools:
            with pool.connection() as conn:
                total += conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
        return total
TEXT
What sharding costs you

joins            across shards: not possible in the database, do it in the app
transactions     across shards: no single ACID transaction
unique keys      globally unique IDs must be generated, not auto-incremented
reporting        every aggregate becomes a fan-out query
rebalancing      adding a shard means moving data while staying online
operations       backups, migrations and monitoring, multiplied

How it works #

The shard key decides everything. Choosing conversation_id means all messages in a conversation live together, so the most common query — recent messages in one conversation — touches exactly one shard.

Hashing the key spreads data evenly. The weakness is that % shard_count changes when you add a shard, remapping almost every key. Consistent hashing solves this by moving only a small fraction.

Range sharding is simpler to reason about but produces hot spots for sequential keys: sharding by auto-increment ID sends all new writes to the last shard, which is the opposite of what you want.

total_messages shows the real cost. Any question that is not scoped by the shard key must be asked of every shard and combined in application code. Aggregates that were one SQL query become fan-out operations.

Globally unique IDs need a different approach, since each shard's auto-increment starts at 1. UUIDs or a distributed ID scheme such as Snowflake are the usual answers.

Uneven distribution is the other practical hazard. A celebrity account or a huge tenant can make one shard far busier than the rest, which is why key choice must consider real-world distribution and not just theoretical uniformity.

Real-world use #

Very large systems shard: social networks by user ID, messaging by conversation, multi-tenant SaaS by tenant ID. In the SaaS case the shard key is obvious and queries are naturally scoped, which makes it far less painful.

Before sharding, the usual ladder is: add indexes, cache aggressively, add read replicas, move heavy columns or tables elsewhere, archive old data, and buy a bigger machine. Modern hardware is large, and most applications never need to go further.

Some databases handle it for you. Vitess, Citus and several managed cloud databases distribute data across nodes while presenting a single interface — considerably less work than sharding in application code.

The strongest practical advice is to choose the shard key deliberately and early if you know you will need it. Changing it later means migrating every row.

Common mistakes #

  • Sharding before exhausting indexes, caching, replicas and vertical scaling.
  • Choosing a shard key that makes common queries cross shards.
  • Sharding by sequential ID, so all new writes hit one shard.
  • Relying on auto-increment IDs, which collide across shards.
  • Forgetting that reporting and admin queries become fan-out operations.

Practice #

Design the sharding for a multi-tenant application where each company has its own users, projects and tasks. Choose a shard key, list the three most common queries and confirm each hits one shard, then identify one query that would require a fan-out and how you would avoid it.

Quick quiz

  1. 1. What problem does sharding solve that replication does not?

  2. 2. Why is sharding by sequential ID a poor choice?

  3. 3. What does consistent hashing improve?

  4. 4. What happens to cross-shard aggregate queries?

  5. 5. What should you try before sharding?

Summary

  • Sharding splits data across databases to scale writes and volume.
  • The shard key determines whether queries stay on one shard.
  • Hash spreads evenly; consistent hashing makes resharding survivable.
  • Joins, transactions, unique IDs and reporting all become harder.
  • Exhaust indexes, caching, replicas and bigger hardware first.