TL;DR

1. The problem: why one database server eventually isn't enough

In the replication lesson we scaled reads by adding replicas. But every replica stores all the data and applies every write. Three limits remain:

The answer is to partition the data: give each server responsibility for a subset of the rows. That's sharding. (Terminology: MongoDB and Elasticsearch say shard; Cassandra says partition or token range; HBase says region; Bigtable tablet; CockroachDB range; Couchbase vBucket. Same idea.)

🧠 Analogy: a city's phone directory When a city's phone book got too thick to print as one volume, publishers split it: Volume 1: A–F, Volume 2: G–M, and so on. Anyone looking up "Patel" knows to grab the volume covering P — that's a range shard with the surname as the shard key. But if half the city is named "Smith", the S volume becomes huge (a hotspot). And a question like "everyone living on Baker Street" means reading every volume (a scatter-gather query), because the books were split by name, not by street.

2. Vertical partitioning vs horizontal sharding

There are two ways to split a database, and real systems use both:

Vertical: split columns / tables Horizontal (sharding): split rows id | username | email | bio | settings id | username | email id | bio | settings hot, narrow, cached cold, bulky Users DBComments DBDMs DB functional partitioning: one database per service each still limited to one machine's capacity user 17 | alice | …user 42 | bob | … user 913 | carol | …user 2048 | dave | … Shard 1users 17, 913 Shard 2user 42 Shard 3user 2048 same schema everywhere; rows placed by shard key
Vertical partitioning splits a table's columns (or splits tables/services into separate databases); horizontal sharding splits a table's rows across servers using a shard key.

3. Choosing a shard key

The shard key (partition key) is the column whose value decides which shard a row lives on. It's the most consequential and least reversible decision in a sharded design — changing it later means moving essentially all the data. Evaluate candidates against these criteria:

  1. High cardinality. Many distinct values, far more than the number of shards. user_id (billions) ✅; country (~200) ❌ — you can never have more than 200 useful shards, and they'd be wildly uneven.
  2. Even distribution of data and load. Not just row counts — request rates too. A key can spread rows evenly yet concentrate traffic (one celebrity's rows get 10,000× the reads).
  3. Query locality. The hottest queries should include the shard key, so they hit one shard. Data that's read together should share a shard key so it's co-located.
  4. No write hotspots. Monotonically increasing keys (timestamps, auto-increment IDs) under range sharding send all new writes to the last shard.
  5. Immutable. If the key value can change (e.g. username, country of residence), the row must move between shards on update. Use a stable surrogate ID.
  6. Supports growth. The scheme must let you add capacity without rewriting everything (Section 6).

3.1 Instagram shard-key decisions

DataShard keyWhy — and what it costs
Users / accountsuser_idEvery profile read, settings change and auth check is by user. Lookups by username need a separate mapping (a global index, Section 8).
Postsauthor's user_idProfile grid ("posts by user X, newest first") becomes a single-shard range query; a user's posts are co-located with their account. Fetching a post by post_id alone works because Instagram-style IDs embed the shard number.
Likespost_id"Who liked this post / how many / did I like it" stays on one shard. "All posts user X liked" then needs a second copy keyed by user_id.
Commentspost_idComments are always displayed per post; keeps question/answer threads in order on one shard (consistent prefix).
Follow graphboth directions: following by follower_id, followers by followee_idEach direction's list is single-shard. A follow writes two shards (handled asynchronously and idempotently — not via 2PC). See Social Graph.
Home timelineviewer's user_id"My feed" is one key — one shard. Fan-out workers write to many shards. See News Feed.
Direct messagesconversation_idA thread's messages are read together and must be ordered. See Direct Messaging.
⚠️ Shard keys that look tempting but fail created_at with range sharding (every new write hits the newest shard); country (low cardinality, massively uneven — one shard for India, one for Iceland); username (mutable); is_private (two values). And a key that's great for one query can be terrible for another — which is why the same fact is often stored twice under two different shard keys.

4. Sharding strategies

4.1 Range-based sharding

Each shard owns a contiguous range of key values: users 1–100M on shard A, 100M–200M on B, and so on (or usernames a–f, g–m…). A small routing table lists the boundaries.

4.2 Hash-based sharding

Apply a hash function to the key and use the hash to pick the shard: shard = hash(user_id) mod S. A good hash (MurmurHash, xxHash, MD5) scatters even sequential keys uniformly.

4.3 Directory (lookup-table) sharding

A directory service stores an explicit mapping key (or key group) → shard. Routing = look up the key, then go to that shard.

4.4 Geographic sharding

Place each user's data in a shard located in (or near) their region: EU users in Frankfurt, Indian users in Mumbai.

 RANGE  (key = user_id)                 HASH  (shard = hash(user_id) mod 4)
   [1 ……… 100M]   → shard A               user 17   → h=…3A → shard 2
   [100M … 200M]  → shard B               user 18   → h=…F1 → shard 1   (neighbours scatter)
   [200M … 300M]  → shard C  ← all new    user 19   → h=…07 → shard 3
                               signups!   user 20   → h=…C4 → shard 0

 DIRECTORY (lookup table)               GEO (by home region)
   tenant "acme"   → shard 7              user in Germany  → EU shard group
   tenant "globex" → shard 2              user in India    → APAC shard group
   user 42 (celebrity) → dedicated shard  user in Brazil   → LATAM shard group
StrategyDistributionRange queriesRouting costReshardingHotspot riskTypical users
RangeCan be skewed✅ ExcellentSmall boundary tableEasy splits of a single rangeHigh for sequential keysHBase, Bigtable, Spanner, CockroachDB, MongoDB ranged
Hash✅ Uniform❌ Scatter to all shardsPure computationPainful with mod N; easy with fixed partitions/consistent hashingOnly per-key (celebrities)Cassandra, DynamoDB, Redis Cluster, MongoDB hashed, Vitess
DirectoryWhatever you chooseDepends on mappingExtra lookup (cached)✅ Most flexible — move any key/groupCan isolate hot keysMulti-tenant SaaS, logical→physical maps
GeoFollows population — unevenWithin region onlyUser → region mapMoving users between regions is costlyBig regionsCompliance-driven systems, multi-region apps

5. Hotspots and the celebrity problem

Even with a perfect hash, sharding balances keys, not traffic per key. A single key can be orders of magnitude hotter than average — and a key always lives on one shard. Instagram's extreme case: accounts with hundreds of millions of followers.

Assume a celebrity posts and the post gets 10 M likes in the first hour.
  10,000,000 / 3,600 s  ≈ 2,800 like-writes/s — all with the same post_id → one shard.
  Peak in the first minutes may be 10× that: ~28,000 writes/s on ONE shard,
  while the average shard handles a few hundred. Meanwhile 50 M viewers read the
  post's like count and comments → hundreds of thousands of reads/s on the same shard.

Mitigations, usually combined:

6. Logical vs physical shards, and resharding

6.1 Why naive resharding hurts

Suppose you start with 4 database servers and route with user_id mod 4. A year later you need 5. Under mod 5, a key stays put only if id mod 4 == id mod 5, which is true for just 4 of every 20 consecutive IDs — 80% of all rows must move, while the system is serving traffic. Doubling from 4 to 8 is kinder (exactly 50% move), but still means copying half your data at once. The consistent hashing lesson works through this arithmetic in detail.

6.2 The fix: many logical shards, few physical servers

Decouple the two concerns:

To grow, you move whole logical shards from crowded servers to new ones and update the mapping. No key ever changes logical shard, so nothing is re-hashed; you only move as many logical shards as needed to balance the new server. Data movement is also simple, because a logical shard is a self-contained unit (a schema, a database, a set of files).

Before: 2 physical servers After: 4 physical servers pg-db-01pg-db-02 L0L1L2L3 L4L5L6L7 4 logical shards4 logical shards(PG schemas)(PG schemas) db-01db-03 🆕db-02db-04 🆕 L0L1L2L3 L4L5L6L7 Routing never changes: logical = user_id mod 8 (in production: thousands of logical shards) Mapping table changes: {L0,L1 → db-01, L2,L3 → db-03, L4,L5 → db-02, L6,L7 → db-04} Only the purple logical shards moved (50% of data, to brand-new servers) — no key was re-hashed.
Logical vs physical shards. Keys map to a fixed set of logical shards; a small mapping assigns logical shards to servers. Adding servers means moving whole logical shards and editing the mapping.

6.3 Instagram's publicly described approach

📸 Instagram's PostgreSQL sharding Instagram's engineering blog post on sharding and IDs (2012) describes the design that this lesson's diagram is based on:
NUM_LOGICAL_SHARDS = 4096                 # fixed forever (13-bit field allows up to 8192)

# Small mapping table: cached in every app server, stored in a config service (ZooKeeper/etcd)
LOGICAL_TO_PHYSICAL = {}                  # logical shard -> database host
for ls in range(NUM_LOGICAL_SHARDS):
    LOGICAL_TO_PHYSICAL[ls] = f"pg-db-{ls // 512 + 1:02d}"   # 8 servers × 512 logical shards

def shard_for_user(user_id: int) -> int:
    return user_id % NUM_LOGICAL_SHARDS

def shard_for_object(obj_id: int) -> int:
    # Instagram-style ID: [41 bits time][13 bits shard][10 bits sequence]
    return (obj_id >> 10) & 0x1FFF

def connection_for_user(user_id: int):
    ls = shard_for_user(user_id)
    host = LOGICAL_TO_PHYSICAL[ls]
    return get_pool(host), f"shard_{ls:04d}"   # query tables inside schema shard_NNNN

# A new post by user 42 is created on shard_for_user(42); its ID embeds that shard,
# so later "GET /p/<post_id>" routes with shard_for_object(post_id) — no directory lookup.

6.4 How many logical shards?

Enough that you'll never need more physical servers than logical shards (e.g. if you might one day run 1,000 servers, choose ≥ 4,000 logical shards so each server can still hold several and you can balance finely), but not so many that per-shard overhead (connections, schemas, file handles, monitoring) explodes. Thousands is the common sweet spot. Elasticsearch, Riak and Couchbase (1,024 vBuckets) and Redis Cluster (16,384 hash slots) all use this "fixed number of partitions" approach.

7. Cross-shard queries, scatter-gather and joins

A query that includes the shard key goes to one shard — fast and isolated. A query that doesn't must be sent to every shard and the results merged: scatter-gather.

 "Find posts with caption containing #sunset created in the last hour"  (no shard key!)

                       +--------------------+
   client ---------->  | query router /     |
                       | coordinator        |
                       +--------------------+
            scatter  /     |      |      \   (same query to all N shards, in parallel)
                    v      v      v       v
               shard 1  shard 2  shard 3 … shard N
                    \      |      |       /
            gather   v     v      v      v   (each returns its top-k; coordinator merges,
                       +--------------------+   re-sorts, applies LIMIT)
                       |  merge + sort + k  |
                       +--------------------+
 Latency = the SLOWEST shard.  Load = N × (one query).  Availability = all N must answer.

Why it's expensive:

Strategies:

  1. Design so hot queries include the shard key. This is the whole point of choosing the key from your access patterns.
  2. Denormalise / duplicate by another key — store likes by post_id and by user_id.
  3. Use a purpose-built secondary system for queries that inherently span everything: search goes to Elasticsearch, analytics to the data warehouse.
  4. Precompute — maintain aggregates (counts, leaderboards) incrementally from events instead of computing them across shards on request.
  5. Bound fan-out — scatter to only the shards that can contain matches (e.g. range sharding by time), use timeouts and return partial results when acceptable.

7.1 Joins across shards

A SQL join between rows on different servers can't be executed by either database alone. Options:

8. Secondary indexes in a sharded database: local vs global

The shard key decides where a row lives. But what about lookups by another column — e.g. "find user by username" when users are sharded by user_id? There are two ways to index a non-shard-key column:

📍 Local (document-partitioned) index

Each shard indexes only its own rows. Writes are simple and transactional (row and index entry on the same shard). But a lookup by the indexed column doesn't know which shard holds the match, so it must scatter-gather to every shard.

🌐 Global (term-partitioned) index

The index is itself sharded by the indexed value: e.g. a usernames table sharded by hash(username) storing username → user_id. A lookup hits one index shard, then one data shard. But a write now touches two shards (data + index), so the index is usually updated asynchronously (eventually consistent) or via a distributed transaction.

Local secondary indexGlobal secondary index
Read by indexed columnScatter to all shards1 index shard (+ 1 data shard)
Write costSame shard, same transactionExtra cross-shard write
ConsistencyImmediately consistentUsually eventually consistent (lag)
Good whenQueries also include the shard key, or are rareFrequent lookups by a non-shard-key column
ExamplesMongoDB, Elasticsearch, Cassandra secondary indexes, DynamoDB LSIDynamoDB GSI (async), Vitess lookup vindexes, Spanner/CockroachDB (transactional)

Instagram example: login by username and @mention resolution happen constantly, so a global index — a username → user_id mapping sharded by username — is the right call. Claiming a username is a single-row insert on that index shard with a uniqueness constraint (the single authority for that value), followed by creating the user row. "Posts tagged at location X" is rarer and fits better in the search/discovery system than as a global index on the posts shards.

9. Distributed transactions: two-phase commit, and why to avoid it

Within one shard you have ordinary ACID transactions. An operation that must atomically change rows on two shards needs a distributed transaction. The classic protocol is two-phase commit (2PC):

 Coordinator                 Shard A (follower list)        Shard B (following list)
     | ---- PREPARE (tx 77) ---->|                                |
     | ---- PREPARE (tx 77) ---------------------------------------->|
     |                           | write intent, lock rows,       | write intent, lock rows,
     |                           | fsync, vote YES                | fsync, vote YES
     | <-------- YES ------------|                                |
     | <-------- YES -------------------------------------------------|
     | (log decision: COMMIT)    |                                |
     | ---- COMMIT ------------->|  apply, release locks          |
     | ---- COMMIT ------------------------------------------------->| apply, release locks
 Any NO or timeout in phase 1 → coordinator sends ABORT to all.
 Coordinator crashes after PREPARE? Shards are stuck holding locks ("in doubt") until it recovers.

Why large systems avoid 2PC on hot paths:

(Consensus-replicated systems like Spanner and CockroachDB make 2PC non-blocking by making each participant a replicated Raft/Paxos group, but the latency cost remains.)

What to do instead:

  1. Design operations to be single-shard — the main reason for co-locating data by shard key.
  2. Sagas: a sequence of local transactions, each publishing an event that triggers the next; if a later step fails, run compensating actions (e.g. release a reserved username).
  3. Transactional outbox: in the same local transaction as the business change, insert an event row into an outbox table; a relay publishes it to Kafka. This guarantees "the DB change and the event either both happen or neither", without 2PC between the DB and Kafka.
  4. Idempotent, retryable consumers so at-least-once delivery converges to the right state.
💡 Example: Alice follows Bob Alice's following list lives on Alice's shard; Bob's followers list lives on Bob's shard. Instead of 2PC: (1) a local transaction on Alice's shard inserts (alice → bob) and an outbox event FollowCreated; (2) a consumer inserts (bob ← alice) on Bob's shard with an idempotent upsert, increments Bob's follower count, and triggers a notification. For a few hundred ms Bob's follower list may not include Alice yet — an acceptable inconsistency.

10. Rebalancing

Rebalancing moves data and load between nodes when nodes are added, removed or overloaded. Goals: afterwards load is fair; during the move the database keeps serving reads and writes; and no more data moves than necessary.

ApproachHow it worksUsed by
Fixed number of partitionsCreate many more partitions than nodes up front; move whole partitions to new nodes.Instagram logical shards, Elasticsearch, Riak, Couchbase vBuckets, Redis Cluster slots
Dynamic splittingA partition that exceeds a size threshold splits in two; halves can move. Partition count grows with data.HBase regions, Bigtable tablets, MongoDB chunks, CockroachDB ranges (~512 MB)
Proportional to nodesEach node owns a fixed number of token ranges (virtual nodes); a new node steals some ranges from existing ones.Cassandra, ScyllaDB (vnodes) — see Consistent Hashing

10.1 Moving a shard without downtime

  1. Copy: snapshot the logical shard to the target server (or start a streaming replica of the source).
  2. Catch up: stream ongoing changes (replication or CDC) until the target lags by only milliseconds.
  3. Cut over: briefly block writes to that shard (seconds), let the target apply the final changes, flip the mapping in the config service, and let routers pick it up.
  4. Verify and clean up: compare row counts/checksums, then delete the shard from the source.

Practical concerns: throttle copies so they don't starve production traffic; prefer human-approved rebalancing plans (fully automatic rebalancing combined with automatic failure detection can cascade — a slow node is declared dead, its data is moved onto other nodes, which become overloaded…); and make sure routers handle "shard moved" errors by refreshing their mapping.

10.2 Request routing

Someone must know the mapping: (a) smart clients that cache the map (Instagram-style app-level routing, Cassandra drivers, Redis Cluster clients following MOVED redirects); (b) a routing tier (MongoDB mongos, Vitess vtgate, Citus coordinator); or (c) any node that forwards to the right one (Cassandra coordinators, gossip). The authoritative map usually lives in a consensus store (ZooKeeper, etcd, a config server).

11. How Instagram uses sharding

📸 Sharding across Instagram's data layer

11.1 A request through the shards

  1. Open profile @bob. The global usernames index (sharded by username) resolves bob → user_id 1872….
  2. logical = user_id mod 4096 = 1337; mapping says logical shard 1337 lives on pg-db-27; a nearby read replica of pg-db-27 serves Bob's profile row and his newest 12 posts from schema shard_1337 — one shard, one index range scan.
  3. Tap a post. The post ID's middle 13 bits say shard 1337 → same server. Like count and comments come from post_id-partitioned stores and caches.
  4. Alice likes it. The like is written to the shard of the post; a copy keyed by Alice's user_id ("posts I liked") is written asynchronously via an event.
  5. Feed. Bob's new post is fanned out by workers to each follower's timeline, keyed by the follower's user_id — touching many shards, but asynchronously and in batches.

Rough numbers (assumptions)

Assume 2 B users, ~1 KB of user row + indexes each       → ~2 TB of user data
Logical shards: 4,096                                      → ~490,000 users and ~0.5 GB per logical shard
Physical servers: 64 primaries (each + 2 replicas)          → 64 logical shards / server, ~32 GB / server
Peak user-DB writes 40,000/s ÷ 64 primaries                 → ~625 writes/s per primary (comfortable)
Growth to 128 servers: move 32 logical shards off each server (~16 GB each), no re-hashing.

Scatter-gather tax: a query without the shard key at 4,096 logical shards
  = up to 64 physical queries per request; at 1,000 req/s → 64,000 queries/s just for that feature.

12. Trade-offs & common pitfalls

✅ What sharding gives you

  • Near-linear scaling of storage and write throughput
  • Smaller datasets per server: indexes fit in RAM, faster backups and rebuilds
  • Fault isolation — a failed shard affects a small fraction of users
  • Ability to place data near users or in required jurisdictions

⚠️ What it costs

  • Cross-shard queries, joins and aggregations become hard or expensive
  • No cheap multi-shard transactions; foreign keys only within a shard
  • Hot shards from skewed keys; rebalancing operations
  • Global uniqueness and secondary lookups need global indexes
  • Much more operational complexity (routing, migrations, schema changes × N shards)

Common pitfalls

13. Real-world technologies

ToolWhat it shardsHowWhen to use
VitessMySQLKeyspaces split into shards by "vindexes" (hash, lookup, range); vtgate routes and scatter-gathers; online resharding workflows. Born at YouTube; used by Slack, GitHub, PlanetScale.Scaling a MySQL application horizontally while keeping MySQL.
CitusPostgreSQL (extension)Distributed tables hash-sharded by a distribution column; co-located and reference tables; coordinator pushes down queries; parallel scatter-gather for analytics.Multi-tenant SaaS and real-time analytics on PostgreSQL.
MongoDB shardingDocumentsRanged or hashed shard keys; data in chunks; balancer migrates chunks; mongos routers; config servers hold metadata. Shard keys can be refined/resharded in newer versions.Document workloads outgrowing a replica set.
Cassandra / ScyllaDBPartitionsMurmur3 hash of partition key on a token ring with vnodes; automatic placement and replication; streaming on node add/remove.High-write, key-access workloads (Instagram feeds/activity).
DynamoDBItemsFully managed hash partitioning on partition key; automatic splits for size and heat ("adaptive capacity").Serverless key-value at any scale on AWS.
Spanner / CockroachDB / TiDB / YugabyteDBSQL tablesAutomatic range splitting + consensus replication + distributed transactions.When you want sharding to be invisible to the app.
Redis ClusterKeys16,384 hash slots (CRC16 mod 16384) assigned to nodes; hash tags {user42} co-locate keys.Sharded caches and timelines.
Application-level shardingAnythingYour code maps keys → logical shards → servers (Instagram's original approach).Full control, simple access patterns, team willing to own the tooling.

14. Interview corner

How would you shard Instagram's data? What's the shard key for each table?

Users, posts and timelines by user_id (author for posts, viewer for timelines), because the hottest queries — profile, profile grid, my feed — are per user and become single-shard. Likes and comments by post_id since they're read per post; a secondary copy of likes by user_id for "posts I liked". Follows stored twice: following by follower, followers by followee. DMs by conversation_id. Usernames in a global index sharded by username. Use thousands of fixed logical shards mapped to physical servers, and IDs that embed the logical shard so any object routes without a lookup.

Range vs hash sharding — when would you choose each?

Range when queries scan contiguous key ranges (time-series, ordered listings) and keys aren't sequentially written — or when the system auto-splits ranges (Bigtable, CockroachDB). Hash when access is by exact key and you need even write distribution, accepting that range queries scatter. Cassandra combines them: hash the partition key for placement, sort by clustering key within the partition. For Instagram's per-user data, hash (or modulo over logical shards) of user_id is natural, with time ordering inside each user's data.

How do you add capacity without moving most of the data?

Never route with hash mod (number of servers). Either (1) pre-split into a large fixed number of logical shards (e.g. 4,096) and map them to servers via a small table, moving whole logical shards to new servers — Instagram's approach — or (2) use consistent hashing with virtual nodes so a new node takes ~1/N of the keys. Move data online: snapshot, stream changes to catch up, brief write freeze, flip the mapping, verify, delete the old copy.

A celebrity's posts overload one shard. What do you do?

First, cache: serve the post, counts and top comments from Redis/Memcached and media from the CDN so reads barely touch the shard. For writes (likes), aggregate counter increments and split the hot key across sub-keys (post_id#0..15) so writes spread across shards. For the feed, use fan-out-on-read for celebrity posts instead of pushing to hundreds of millions of timelines. If an account is persistently hot, move its logical shard to dedicated hardware via the mapping table.

How do you look up a user by username when users are sharded by user_id?

Build a global secondary index: a username → user_id table sharded by (hash of) username. Lookup = one index shard + one user shard. Usernames are unique, so the index shard is also the single authority that enforces uniqueness (an insert that fails on conflict). Because the index and the user row are on different shards, write the index first as a "reservation", then the user row, and clean up the reservation if sign-up fails (saga-style), rather than 2PC. A local index would require querying all shards on every login.

Why avoid two-phase commit, and what do you use instead?

2PC blocks if the coordinator fails after participants vote yes (locks held "in doubt"), adds multiple round trips and forced log writes, needs every participant available (availability multiplies down) and holds locks across the network, killing throughput. Instead: co-locate data so operations are single-shard; use sagas with compensating actions; use the transactional outbox to publish events atomically with local changes; make consumers idempotent. For follow/unfollow, write locally plus an outbox event; the other shard is updated asynchronously.

What's the difference between sharding and replication? Do you need both?

Replication keeps copies of the same data on several nodes — for availability, durability and read scaling; it doesn't scale writes. Sharding splits data so each node owns a subset — for data size and write scaling, and it reduces blast radius but doesn't by itself provide redundancy. Production systems combine them: each shard is a small replicated group (a primary + replicas, or RF=3 in Cassandra).

15. Check your understanding

You route with user_id mod 10 across 10 servers and add 1 server (mod 11). Roughly what fraction of users move?

A key stays only if id mod 10 == id mod 11, which happens for 10 of every 110 consecutive IDs ≈ 9%. So ~91% of users move. With 4,096 logical shards on 10 servers, adding an 11th means moving only ~1/11 ≈ 9% of the data (the logical shards assigned to the new server).

Why is created_at a bad shard key with range sharding but less bad with hash sharding? Why is it still a poor choice for Instagram?

With range sharding every new write goes to the latest time range → one hot shard. Hashing spreads writes evenly. But Instagram's hot queries are "by user" or "by post", not "by timestamp", so with either scheme almost every query becomes scatter-gather. The shard key must match the access pattern.

A scatter-gather query hits 50 shards; each shard independently exceeds 200 ms with probability 2%. What's the probability the whole query exceeds 200 ms?

1 − 0.9850 ≈ 1 − 0.364 = 0.636 → about 64%. Fan-out turns a rare per-shard slowdown into the common case — one reason to keep hot queries single-shard.

Instagram IDs are [41 bits time][13 bits shard][10 bits sequence]. How many logical shards and how many IDs per shard per millisecond can this support? How do you extract the shard?

213 = 8,192 logical shards; 210 = 1,024 IDs per shard per ms (~1M/s per shard). Shard = (id >> 10) & 0x1FFF. The 41-bit millisecond timestamp covers about 69 years from the custom epoch.

Users are sharded by user_id. Which is single-shard: (a) load Alice's settings, (b) find user by email, (c) Alice's 12 latest posts, (d) count all users who joined today?

(a) and (c) include the shard key (posts are co-located by author). (b) needs a global email index (or scatter-gather). (d) is an aggregate across all shards — compute it from an event stream or in the data warehouse.

What's the difference between a local and a global secondary index, and which would DynamoDB's GSI be?

A local index lives on each shard and covers only that shard's rows (cheap consistent writes, scatter-gather reads). A global index is partitioned by the indexed value (single-shard reads, cross-shard writes, usually async). DynamoDB's Global Secondary Index is a global index, updated asynchronously, so its reads are eventually consistent.

Your shard map lives in every app server's memory. A logical shard is moved, but a few app servers still have the old map. What prevents corrupted writes?

During cut-over the source shard is made read-only (or the schema dropped/fenced) before the map flips, so stale writers get errors instead of silently writing to the old location; the error makes them refresh the map (like Redis Cluster's MOVED redirect). Maps should be versioned, and routers should re-fetch on version mismatch.

🎯 Key takeaways

Related lessons