System Design Data
Data Partitioning
An Uber rider taps request and the matching service must find nearby drivers without scanning the entire planet. Instagram must open one user's timeline without reading every post on earth. Both problems are the same shape: so each query touches a narrow slice.
Warehouses and OLTP engines automate splits differently, but the trade-offs: range vs hash vs composite, hot shards, and index fan-out: recur everywhere at scale.
Key decision path
Name the hottest query
What field almost always appears?
Check cardinality
Enough distinct values to spread load?
Celebrity tenants need salts or queues.
Pick range, hash, or composite
Match scan needs to placement.
What actually happens
You choose a . Rows with the same key land together. Reads that include that key stay local; reads that omit it scatter across shards or need a secondary index.
keeps ordered neighbors together. spreads keys evenly but loses global order. A often wins for timelines: hash or route by entity, then sort by time inside the shard.
Growth is not only more disks. You plan partition size, per-shard QPS, rebalance workflows, and what happens when one tenant becomes a .
Try it: see partition skew appear
Pick what you hash on, then replay a traffic mix with an optional celebrity spike. Shard bars show placement load, not storage bytes.
- Requests routed
- 120
- Hottest shard QPS share
- 14%
- Skew ratio (max/min)
- 1.2
- Idlest shard count
- 3
Shard 0
15 requests in this sample
Shard 1
15 requests in this sample
Shard 2
15 requests in this sample
Shard 3
14 requests in this sample
Shard 4
14 requests in this sample
Shard 5
14 requests in this sample
Shard 6
16 requests in this sample
Shard 7
17 requests in this sample
Hash by user spreads a mixed tenant mix evenly. Partition by tenant alone concentrates a viral tenant on one shard until you salt or queue hot keys.
Start balanced. Use hash-by-user without the burst. Skew ratio stays near one because keys are numerous and evenly distributed.
Switch to tenant key. Enable the celebrity burst. One bar grows while siblings idle, mimicking a hot partition on a shared database or sharded cache.
Add salting. The same burst spreads across four salted buckets. You traded scatter-gather on reads for even write load, a common compromise for mega-tenants.
How to choose a partition key
1. Does the hottest query include this key? If not, expect scatter-gather or add an index. Start from access patterns, not from a random column.
2. Is cardinality high and traffic even? Low-cardinality keys create hotspots. Large tenants may still need salting even when the key looks "correct."
3. Do you need ordered scans? Prefer composite keys for per-entity ranges. Prefer hash when even write spread matters more than cross-key order.
Simple rule of thumb
Choose high-cardinality keys that appear in common reads and writes. Keep related mutations on one partition when you can. Prefer a or co-location over chatty cross-partition transactions unless money or correctness forces stronger coordination.
Partition strategies
Partitioning divides one logical dataset into smaller ownership or management units. It may happen inside one database machine, where partition pruning reduces scan work, or across machines, where it becomes sharding. Range partitioning keeps adjacent values together for scans; hash partitioning spreads distinct keys more evenly; a directory maps keys or tenants to explicitly chosen owners. The best choice depends on how queries locate their data.
Pick the partition key to match your hottest queries. Anything else pays scatter-gather cost or needs an alternate index.
Contiguous key intervals live together: friendly for scans ordered along that dimension (time slices, lexicographic entity IDs). Wide-column stores and some globally distributed SQL engines migrate contiguous directories as units.
Risk: append-only time keys concentrate writes on the leading partition unless salted or rotated.
partition = hash(key) mod N spreads keys pseudo-evenly. Many managed NoSQL systems derive partitions from hashes. Ordering across keys is lost unless you add a separate sorted component per partition.
Trade-off: range queries without the partition key fan out.
Hash (or route) by high-cardinality entity id, then sort inside the shard:PRIMARY KEY ((user_id), timestamp)style: balances spread while preserving efficient time-range reads per user.
Often the pragmatic default for per-tenant timelines.
Directory-based routing
A routing layer maps keys to shards explicitly: maximum flexibility for rebalancing without changing the hash function. Must itself be replicated for High Availability (HA).
Risk: metadata service becomes critical infrastructure; use versioned maps and cache invalidation carefully.
Beginner decision flow: does this key deserve to partition data?
Start from access patterns, not from a random column. A useful partition key appears in common reads/writes, has enough distinct values, and avoids one "celebrity" value dominating traffic.
Pick partition key
Does the hottest query include this key?
No
Expect scatter gather or add an index
Revisit access patterns
Yes
Check cardinality next
High cardinality and even traffic?
No
Risk hot partitions add salt or composite key
Revisit access patterns
Yes
Check range needs next
Need range scans in order?
Yes
Composite key hash entity then sort by time
No
Hash key for even spread
Good candidate
Hot partitions and mitigation
Salting adds a secondary bucket to a hot logical key, distributing writes across several partitions. For a tenant producing 5,000 writes per second with a 2,000-per-partition safe capacity, three perfectly balanced buckets are a mathematical minimum, not comfortable reserve. Reads may now query all buckets and merge results. Select the salt count from measured skew and failure headroom, and preserve a way to determine the bucket for point lookups.
Skew comes from viral keys: a celebrity profile, a flash-sale SKU, a leaderboard artifact, not only from a naive partition key. Combine adaptive splitting with application tricks like write sharding and read aggregation.
When one logical shard absorbs disproportionate traffic, autoscaled tables may borrow capacity, isolate hot items, or split underlying partitions. Behavior differs by storage engine; instrument partition metrics early.
Hot partition math
If total writes are 100K/s and one tenant owns 5%, that tenant produces 5K writes/s. With a safe limit of 2K writes/s per partition, salt_count = ceil(5000 / 2000) = 3. Use at least three salted partitions, then remember reads may fan out to three unless you pre-aggregate.
Write sharding
Spread hot logical keys across suffix buckets (e.g. profile:id:{0..99}); aggregate on read. Great for writes, costlier for reads.
Adaptive capacity
Managed tables may borrow throughput or isolate partitions; behavior depends on the storage engine.
Split / merge
Automatic splits when size or queries per second (QPS) crosses thresholds; watch split storms.
Counter shards
Fan-out writes to likes:post:{id}:shard_n; total = sum of shards.
Celebrity profile
Read storm on one user id; cache, replicate reads, or materialize public profile snapshots.
Flash-sale SKU
Write storm on one item; queue reservations or shard counters with reconciliation.
Large tenant
One customer owns 5% of traffic; route tenant to multiple virtual shards.
Current timestamp
Append-only writes hit the newest range; add hash prefix or rotate buckets.
Partition sizing and growth math
Partition design is capacity planning: estimate bytes, writes, reads, and the cost to move data when a shard grows too large or too hot.
Daily writes
Logical partitions
Too large or too hot?
No
Stay on current shard
Size
Split range or time bucket
Traffic
Add salts or virtual shards
Move selected partitions
Target partition size
Pick a practical target such as 10-50 GB for operational shards or 128-512 MB for analytical files. Smaller units rebalance faster but increase metadata.
Million-user timeline
1M users x 20 writes/day x 2 KB ≈ 40 GB/day. With 30-day hot retention, plan around 1.2 TB hot plus indexes and replicas.
Hot user or tenant
If one tenant produces 5% of traffic, cluster averages lie. Salt that tenant into 16-64 virtual partitions and merge results on read.
Rebalance window
Moving 500 GB at 100 MB/s takes about 1.4 hours before verification. Throttle below customer traffic headroom, not link maximum.
Sizing formula
partitions = ceil(total_hot_bytes / target_partition_bytes), then check per-partition QPS and top-key skew separately.
Capacity worksheet
logical_partitions = ceil(total_hot_bytes / target_partition_bytes)
qps_per_partition = total_qps / active_partitions
top_key_qps = total_qps x top_key_fraction
salt_count = ceil(top_key_qps / safe_qps_per_salt)
read_fanout = salt_count unless cache or pre-aggregation reduces it
1M-user hot retention example
1M users x 20 writes/day x 2 KB = 40 GB/day. For 30-day hot retention, that is 1.2 TB raw. With 3 replicas and 40% index overhead, plan about 1.2 TB x 3 x 1.4 = 5.04 TB of hot physical footprint.
Rebalancing workflow
Migration is a change in ownership while traffic continues. Copy the data, capture concurrent changes, verify the destination, and switch routing at a controlled point. The source must stop acting as an independent writer after handoff. Keep redirects or routing refreshes for stale clients, and track lag, copy bandwidth, checksums, and user latency. A failed migration should leave a clear authoritative owner, not two partially updated copies.
Moving a shard is not only copying bytes. You must keep writes correct, rebuild indexes, validate results, and flip the routing map safely.
1. Prepare destination
Create the destination shard and reserve write, read, and storage headroom.
2. Copy snapshot
Copy the selected range or logical shard while source continues serving traffic.
3. Catch up changes
Use dual-write or change-data-capture to apply writes that arrived during the copy.
4. Validate
Compare counts, checksums, index rows, and sampled reads.
5. Flip map
Publish a new versioned routing map and make clients refresh or respect redirects.
6. Drain source
Keep rollback data briefly, then remove old copies after the safety window.
Secondary index cost
Local indexes move with the partition. Global indexes duplicate writes and may lag, so repartitioning can require rebuilding dependent index shards and validating them separately.
Choosing partition keys under pressure
State the access pattern first, then the key. Interviewers are checking whether your choice reduces fan-out without creating a new hotspot.
User timeline
(user_id), timestamp
Most reads ask for one user over a time range; composite sort key keeps pagination local.
Orders by merchant
(merchant_id), created_at
Merchant dashboard scans stay narrow, but large merchants may need salting by day or order bucket.
Global log analytics
date/hour + randomized file prefix
Time pruning helps scans; randomized prefixes prevent all writers from hitting one object-store path.
Flash-sale inventory
sku alone is dangerous
One SKU can go viral; use reservation queues, counter shards, or single-writer coordination.
Say this when challenged
"I would verify cardinality and top-key traffic with production-like metrics, monitor per-partition p99/throughput, and keep a rebalancing path through virtual shards or a routing table."
Secondary indexes: local vs global
A local index organizes data already inside one partition, so a query still needs the partition key to route efficiently. A global index can answer a different access pattern across the dataset, but maintaining it creates additional writes and possibly a separate consistency boundary. If an order is partitioned by customer but looked up by seller, decide whether the seller index can lag or must update atomically with the order.
Local indexes stay collocated with the base partition: cheap writes, awkward fan-out if you omit the partition key on reads. Global indexes reorder data under new partition keys: faster alternate lookups, async propagation costs.
Indexed rows live beside primary rows on the same partition. Writes stay transactional with the base record. Omitting the table partition key in a query may scan many shards.
Local-index pattern: same partition key, alternate sort key; constraints often apply at creation time.
Materialized under independent partition keys: queries can target one shard when keyed correctly. Propagation is often asynchronous; budget duplicated throughput.
Global-index pattern: alternate partition key, added later in many designs after access patterns stabilize.
Access-pattern sketches
Orders by order_id
Same partition key ⇒ cheap reads inside one shard because alternate sort keys stay collocated.
“Shipped today” without scanning everything
Add a global index: partition status, sort order_date: targets one alternate shard fan-out.
No supporting index
Full table / shard scans: tail latency tracks your slowest partition.
Cross-partition work
Prefer designs where related mutations touch one partition. Distributed transactions exist where money demands them. They coordinate replicas across shards at latency cost.
Single-partition writes serialize cleanly. Cross-partition paths typically involve two-phase commit, consensus clocks, or compensating sagas.
Two-phase commit (illustrative)
- Prepare: coordinator asks each shard whether it can commit.
- Commit / abort: unanimous YES ⇒ commit everywhere; any NO ⇒ roll back.
- Risk: coordinator loss mid-flight can block participants until recovery. That is why apps favor sagas across services.
Saga pattern (microservices default)
Ordered local commits with explicit undo steps:
If inventory fails → compensate payment and mark the order cancelled (each step idempotent).
Prefer co-location
- Pack related aggregates under one partition key when queries naturally align.
- Single-table and entity-centric models optimize for this.
- Every cross-partition join or transaction is a bill you pay in latency and ops.
Globally ordered timestamps (concept)
Some systems bound clock uncertainty with hardware references so transactions pick globally meaningful commit timestamps: expensive infrastructure, powerful when cross-region serializability is non-negotiable.
Time-based and geo partitioning
Time partitions make retention cheap because an expired interval can be removed as a unit, and time filters can skip irrelevant partitions. The newest interval can become a write hotspot, so combine time with a tenant or hash bucket when needed. Geographic partitioning improves locality but requires an ownership rule for users who travel or records shared across regions. A query must include enough placement information to avoid scanning every region.
Time partitions prune scans and allow dropping whole intervals. Geo partitions pin rows near users or inside compliance boundaries.
Time windows
Logs and metrics benefit from RANGE partitions by month or day: queries over recent windows skip historical files entirely.
CREATE TABLE events (
id BIGINT,
event_time TIMESTAMPTZ,
data TEXT
) PARTITION BY RANGE (event_time);
CREATE TABLE events_2026_03 PARTITION OF events
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
SELECT * FROM events WHERE event_time > '2026-03-10'; -- prunes old partitions
DROP TABLE events_2025_01; -- bulk retireGeo placement
Pin EU tenants to EU replicas for latency and residency rules; some databases expose LIST PARTITION constraints tied to region columns.
-- Illustrative geo constraint sketch:
ALTER TABLE users PARTITION BY LIST (region) (
PARTITION eu VALUES IN ('eu-west', 'eu-central'),
PARTITION us VALUES IN ('us-east', 'us-west')
);
-- Tie partitions to regional replica sets via zone configsFixed shards vs dynamic splits
Over-provision logical shards up front for painless moves, or let the store split heavy partitions automatically: each approach trades operational simplicity against prediction risk.
Fixed logical shards
Thousands of virtual shards mapped onto fewer nodes: add hardware by moving shard bundles without reshuffling every row (patterns in search and key-value systems).
Dynamic splitting
Regions split when byte size or throughput crosses thresholds; merges reclaim idle space. Operators monitor split storms during traffic spikes.
Directory moves
Some globally distributed SQL engines relocate contiguous key directories between Paxos groups for fine-grained load balancing.
Patterns at scale
Large social graphs often hash user ids into thousands of logical shards backed by pools of databases; ride platforms partition by city-day so localized queries stay narrow.
User-centric OLTP
shard = hash(user_id) % N; routing service maps logical shard → physical database.
Adaptive tables
Viral partitions borrow capacity or split: monitor per-partition metrics, not cluster averages only.
Geo-time analytics
Keys like (city_id, date) keep “rides in NYC today” on a narrow slice of data.
Degraded modes and rebalance drills
Partitioning spreads load until a hot key, split storm, or bad map update concentrates it again. Predefine how the product behaves while you rebalance.
Hot partition
Rate-limit that tenant/SKU, salt or queue writes, serve cached reads, and avoid global retries that amplify the hotspot.
Directory / map unavailable
Cached routing maps keep reads going briefly; fail closed on writes that need authoritative placement until the control plane recovers.
Split / move in progress
Dual-write or change-catchup must finish before cutover. Prefer read-repair and redirects over serving half-migrated ranges.
Cross-partition fan-out storm
Shed analytics and wide scans first; keep single-partition user paths. Cap fan-out concurrency per request.
Test and drill notes
- Inject a 5% traffic tenant and verify salt_count and per-partition QPS ceilings hold.
- Rebalance drill: move one shard under load with throttle; validate checksums before flipping the map.
- Rule of thumb: state partition key, expected QPS/bytes per partition, and the hot-key mitigation before naming a database product.
Takeaway
Choose high-cardinality partition keys, model composite keys for per-entity timelines, plan mitigations for viral skew, add global secondary indexes (GSIs) or warehouse projections only where access patterns justify cost, geo-pin sensitive tenants, and monitor per-partition metrics, not just cluster averages.
Primary sources
- Corbett et al.: Spanner (OSDI 2012): global partitions and TrueTime.
- CockroachDB Docs: Partitioning: geo/range partitioning practice.
- PostgreSQL Docs: Table Partitioning: declarative partitioning.
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Choose horizontal, vertical, range, hash, or directory partitioning with explicit skew thresholds, global-order/index cost, and split/merge recovery.
Explain the decision
When is range partitioning preferable to hashing?
Read the reasoning
When range scans and time pruning dominate and hotspots can be controlled; hashing balances writes but destroys natural range locality.
Apply it to a production scenario
Today’s time partition receives all writes while older partitions are idle. How do you spread load?
Read one approach
Add a hash suffix within the active time range, buffer/batch writes, split the hot range, or partition by a higher-cardinality ownership key.
Work through the numbers
A 9 TB table grows 300 GB/month; partitions should stay below 750 GB for maintenance. Estimate initial partitions.
Read the worked calculation
At least 12 today; plan additional partitions or automated splits for growth and keep per-partition replicas/headroom in the model.
Reason through a failure
What must directory-based partitioning survive?
Read the recovery reasoning
Directory outage or stale versions must not create two write owners; cache maps with versions and fence writes during ownership transfer.
Sources and further study
Continue with Data Lakes & Warehouses.