Your Shard Key Already Has an Expiration Date — You Just Don't Know It Yet
Day 10: A Shard Key Isn't a Decision. It's a Contract You'll Have to Renegotiate.
You'll pick a shard key exactly once, with total confidence, using this quarter's traffic numbers. Then traffic will do what traffic does — it will change. The key doesn't get that memo. Everything in this lesson lives in the gap between those two facts, and it's the gap that separates architectures that survive growth from ones that get rewritten under fire.
3am, mid-migration
One shard is running at 4x the write volume of its neighbors. Not a random hot key — the shard key itself, the same customer_id mod N from the original design doc, the one somebody labeled 'permanent' in a Notion page six months back. Every new customer lands on the same three shards, because the biggest new accounts all signed in the same quarter. The 'permanent' scheme is now doing exactly what it was built to prevent: creating a single point of failure. Somebody is writing a live migration script at 3am, moving rows between shards with zero downtime, while the on-call channel fills with p99 alerts. None of this is a bug. This is just what happens to a shard key that worked.
Why the key looked correct six months earlier
Nobody was sloppy here. customer_id was the obvious partition — every query in the app already filtered by customer, cross-shard joins were rare, and the design doc had a clean diagram to prove it. The team even ran the math: projected write volume, projected shard count, comfortable headroom. What the doc didn't have, because it couldn't have, was six months of real customer acquisition. The assumption — 'customers arrive roughly independently and uniformly' — held up fine on paper and fell apart the moment sales closed three whale accounts in the same signup cohort. The reasoning wasn't wrong. It was scoped to information that didn't exist yet.
The actual failure: shard keys are a bet, not a fact
This is where most teams get it backwards. A shard key gets chosen against today's access pattern, but an access pattern is a description of the future — how the system will be queried and written to as it grows — and the future is a bet, not a measurement. Every shard key comes with an expiration date built in. You don't get to see the date. You find out retroactively, usually via an incident. Picking a key that turns out wrong isn't the mistake. Designing as if it couldn't turn out wrong — that's the mistake.
And this isn't unique to databases. It's the same failure mode when you hand a team a fixed 'owner' shard, or route agent tasks to a worker pool by a static rule — tenant ID, region, model family, whatever. The rule is correct for the traffic you have on the day you write it. It stops being correct the moment traffic shape shifts — and in an AI product, shape shifts fast. One customer adopting agentic workflows can blow past a normal SaaS growth curve in days instead of quarters, in a way no human-driven customer ever would.
The fork: bedrock schema vs. versioned contract
Two teams pick the exact same shard key. Six months later, their 3ams don't look anything alike.
- ▹Team A treated the shard key as bedrock — baked into primary keys, embedded in URLs, hardcoded into twelve services' connection logic. Resharding means touching every one of those call sites, under load, with no rollback if the new distribution turns out to be wrong too. Their 3am is a multi-day, all-hands incident with a customer-facing outage.
- ▹Team B treated the shard key as a versioned, migratable contract from day one: a thin indirection layer between the logical key an app sees and the physical shard it actually lands on, dual-writes during migration, shadow reads to check correctness before cutover, and a feature flag to shift traffic gradually. Their 3am is one engineer watching a dashboard, finger on a flag they can flip back if the numbers look off. Tedious, sure. But not an incident.
The difference was never the key they chose. It was whether resharding was a designed capability or an emergency improvisation.
The technique: building resharding optionality before you need it
None of this is exotic engineering. It's four pieces of plumbing that cost real effort up front and pay for themselves exactly once — the day you turn out to be wrong about the key.
- ▹Indirection layer: applications and agents never compute physical shard placement themselves. They ask a shard-map service or library, 'where does this logical key live right now.' This one seam is what makes every other technique below possible — remove it, and every downstream service is welded to the physical scheme.
- ▹Dual-write during migration: writes go to both the old and new physical location for the keys being moved, so you can cut reads over gradually instead of forcing one hard, risky, stop-the-world switch.
- ▹Shadow reads: read from the new location purely to compare against the old — never serve it — so you catch correctness bugs in the migration logic before a single customer sees them.
- ▹Feature-flagged cutover: move traffic percentage by percentage, per shard or per tenant, behind a flag you can flip back instantly the moment the new distribution behaves worse than the old one.
- ▹Backfill/replay tooling: a resumable, idempotent job that moves historical data into the new shard layout without locking the whole table, and that you can safely re-run if it gets interrupted halfway through.
Map this onto what you're already running in an AI system: sharding vector DB collections by tenant, routing agent sessions to worker pools by conversation ID, partitioning KV-cache or session state across inference nodes. The same indirection-plus-dual-write pattern is exactly how you move a hot tenant off shared inference capacity onto dedicated capacity without dropping their in-flight agent runs. That's a resharding event wearing a different name.
# Logical key never maps to a physical shard directly in app code.
def get_shard(logical_key: str) -> Shard:
mapping = shard_map.lookup(logical_key) # versioned, hot-reloadable
return physical_shards[mapping.shard_id]
# During migration, writes fan out; reads are flag-controlled.
def write(logical_key, payload):
old = get_shard_v1(logical_key)
old.write(payload)
if migration.in_progress(logical_key):
new = get_shard_v2(logical_key)
new.write(payload) # dual-write
def read(logical_key):
if cutover_flag.enabled(logical_key):
result = get_shard_v2(logical_key).read()
if migration.shadow_mode:
compare_async(result, get_shard_v1(logical_key).read())
return result
return get_shard_v1(logical_key).read()Back to the course arc
Days 1–9 gave you the vocabulary to pick a partition scheme correctly. Day 10's point is narrower, and it matters more: picking the scheme correctly was never the deliverable. The deliverable is the seam that lets you repartition without a rewrite. Nobody grades your solution architecture on the first shard key you chose — nobody has enough production data on day one to get that right with certainty anyway. They grade it on whether you built the system assuming you'd eventually be wrong.
The reframe
Good sharding doesn't mean you never reshard. It means resharding is boring — a scheduled maintenance ticket, not an incident. If your team's shard key has never had to move, that's not proof you chose well. It's just proof you haven't grown enough yet to find out.
Extend your knowledge
- ▹Read Vitess's resharding docs — this is what runs at YouTube/Slack scale — to see a production-grade version of the indirection-layer-plus-backfill pattern for MySQL.
- ▹CockroachDB does this with automatic range splitting and rebalancing; Vitess does it with online resharding via VReplication. Two different takes on the same idea: shard boundaries are mutable, not fixed.
- ▹Running a vector database — Pinecone, Weaviate, Qdrant — for agent memory or RAG? Check its documented approach to re-sharding and rebalancing collections as tenant data grows unevenly. Same contract-not-bedrock problem, just wearing a vector-DB costume.
- ▹Next time you write a design doc with a partition scheme in it, add a section called 'How do we move off this key' before anyone approves it. Make it a required field, not an appendix.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.