When there's more data or load than a single server can handle, you slice it into pieces — partitions (aka shards). The rule is simple: each record lives in exactly one partition, and the partitions are spread across different nodes. The practice of sharding we cover on MongoDB — replica set, shard key, chunks, balancer. This article is one level up and not tied to a database: how to choose the way to split, what happens to secondary indexes, how to move data when adding nodes, and how a client even figures out which partition a record is in. These questions are the same for MongoDB, Cassandra, Elasticsearch, and HBase — only the names change.
The goal is evenness; the enemy is the hot spot
The point of partitioning is one thing: spread the data and the load across nodes evenly, so that ten nodes handle ten times as much as one. If the split comes out uneven — some partitions got a disproportionate amount of data or requests — it's called skewed, and an overloaded partition is a hot spot. In the worst case all the load lands on one node while the other nine sit idle — and the whole point of sharding is lost.
There are two basic ways to decide which partition a record goes to — by key range and by key hash. In essence these are two different answers to one question: how to avoid a hot spot.
Range partitioning versus hash partitioning
By key range. Each partition gets a continuous stretch of keys — like the volumes of a paper encyclopedia: A–C, D–F, and so on. The boundaries are chosen to fit the data, because the distribution is uneven (there are different numbers of words starting with "A" and with "X"). The upside of this split is range queries: within a partition the keys are in order, so "all events for March" reads as one fast scan. The downside is that the same order easily breeds a hot spot. If a key starts with a timestamp, then all of today's writes land in one partition (today's range) while yesterday's nodes stand idle. Cured by a composite key where time isn't first: say, the sensor name first and only then the time — then writes for one moment spread across different sensors.
By key hash. A good hash function turns even similar keys into evenly scattered numbers, and a partition gets a range not of the keys themselves but of their hashes. Evenness comes almost for free — that's what Cassandra and MongoDB do. The price is losing the sorting: keys that are neighbors in meaning fly off to all partitions, and a range query now has to poll every node. Cassandra takes a compromise — a composite key: only the first part is hashed (it determines the partition), while the rest work as a sorted index within the partition. Then "all of one user's messages over a period" reads efficiently, while the users themselves are spread evenly across the cluster.
There's a separate trouble that neither hash nor range fixes — the hot key. For example, a celebrity with millions of followers: all the traffic hits one of their records, and the hash of the same key is always the same, so you can't "spread it out" automatically. Only the application helps here: glue a random suffix of a hundred variants onto the key — then the write spreads across a hundred partitions. But reads pay for it: now, to gather everything under that key, you have to poll all hundred partitions and stitch the results. So the trick is applied to a handful of known hot keys, not to everything.
Secondary indexes: local versus global
As long as we access data by primary key, everything is simple: it determines the partition. But the application also needs secondary indexes: "find all red cars," "all articles with the word hogwash." A secondary index doesn't line up with the partitioning, and there are two ways to slice it — with diametrically opposite costs.
Local index (document-partitioned). Each partition keeps a secondary index only over its own documents. You put a red car in partition 3 — the entry "color: red" appears in partition 3's index and nowhere else. The write is cheap: everything is local, one partition is touched. But the read is scatter/gather: red cars are scattered across all partitions, so the query "find red ones" has to be sent to every node and the answers stitched together. That's how MongoDB, Cassandra, Elasticsearch work. Scattered reads lengthen the latency "tail" (you wait for the slowest node of all), but the write stays local — which is why this is the default.
Global index (term-partitioned). Here there's one index for the whole cluster, but it too is sliced into partitions — by the value being searched. The whole entry "color: red" sits in one partition of the index. The read is fast — you go to one partition instead of all. But the price moves to the write: adding one document with several fields touches several index partitions at once (the values "color" and "make" live in different places), and keeping that consistent on the fly is a distributed transaction. So global indexes are updated asynchronously: after a write, the change appears in the index with a delay (that's how DynamoDB works — "usually a fraction of a second, longer during faults").
The choice is exactly like everywhere else: a local index means cheap writes, expensive reads; a global one is the reverse. For most systems the default is local.
Rebalancing: how to move partitions between nodes
Over time nodes are added (load grew) and removed (a fault or decommission). Moving partitions from one node to another is called rebalancing, and you expect three things from it: afterward the load is even again; during it the database keeps reading and writing; no more data is moved than actually necessary.
- How not to do it —
hash mod N. It's tempting to assign a partition by the formulahash(key) % N, where N is the number of nodes. But change N, and almost all keys change partition: going from 10 to 11 nodes reshuffles practically all the data. Rebalancing becomes unbearable. - A fixed number of partitions. You create far more partitions than nodes (say, 1000 partitions across 10 nodes) and hand out many per node. A new node simply "takes over" a few partitions from the existing ones. Whole partitions move; their count and the key assignment don't change — only which node holds which partition changes. That's how Elasticsearch, Riak, Couchbase do it. The pitfall: the number of partitions has to be guessed in advance — it sets the ceiling for growth, and if chosen too large it brings extra overhead.
- A dynamic number of partitions. A partition outgrew a threshold — it splits in two; it shrank — it merges with a neighbor (just like a node in a B-tree). The number of partitions adjusts itself to the volume of data. That's how HBase, MongoDB (chunks), RethinkDB work. The empty-database pitfall: at the start there's one partition, and all writes fly to one node until the first split happens (cured by pre-splitting, if the distribution is known ahead of time).
And a separate question — automatic versus manual control. Fully automatic rebalancing is convenient but dangerous paired with automatic fault detection: an overloaded node responds slowly → the system decides it "failed" → starts a rebalance → adds load to the already-overloaded node → a cascading failure. A human in the loop ("the system proposes, an admin confirms," as in Couchbase and Riak) is slower but saves you from such surprises.
Request routing: where does "foo" live?
Partitions are spread across nodes and move during rebalancing. How does a client know which node to go to for the key foo? This is a special case of the general "find who's responsible for what" problem (service discovery), and there are three solutions:
- The client knocks on any node. If the key is there — the node answers itself; if not — it forwards the request to the right node itself and returns the answer. That's how Cassandra and Riak do it, via a gossip protocol: the nodes exchange the cluster map among themselves, and no external coordinator is needed.
- A separate routing tier. All requests go through it, and it knows which node is responsible for what (in MongoDB that's mongos). It doesn't process the requests itself — it's a load balancer that simply knows where each partition is.
- The client itself knows the layout and connects to the right node directly, without intermediaries.
In all three cases someone has to know the current "partition → node" map and learn about changes in time. The classic solution is a separate coordinator service, ZooKeeper: nodes register in it, routers subscribe to changes, and when a partition moves, ZooKeeper notifies everyone. That's how HBase, SolrCloud, Kafka do it. The alternative is that same gossip (Cassandra, Riak): the nodes are more complex, but there's no dependency on an external coordinator.
Where this applies
The "how to shard" fork comes up exactly when one node stops coping. And nearly every decision here is irreversible: the shard key is very hard to change later, and you can't re-do the index scheme after launch. The practical frame: range partitioning — when you need scans by time or order and you're ready to fight the hot spot with a composite key; hash — when evenness matters more; local secondary indexes — the default; global — only if reads by a secondary key are critical and an asynchronous index update is acceptable.
Where beginners stumble:
- The partition key starts with a time or an auto-increment — and all fresh writes land in one partition. The classic hot spot. The first part of the key needs something varied.
- Expecting a secondary index to be as fast as the primary — but with local indexes any query not by the partition key turns into a scatter/gather across all nodes.
- Assigning partitions via
hash mod N— and the first change in the number of nodes reshuffles almost all the data. You need a fixed or dynamic number of partitions. - Combining fully automatic rebalancing with automatic fault detection — a recipe for a cascading failure. Keep a human in the rebalancing loop.
- Forgetting about the hot key — one celebrity or one popular product takes down a partition, even though the distribution formally looks even.
What to read next: MongoDB replication and sharding — shard key, chunks, balancer, and mongos in practice; replication models — partitioning almost always goes together with replication; the building blocks — where sharding lives in the bigger picture.