Tech
Tech · ◉ Evergreen

You can't just change the table

by Shreyansh Ojha·5 min·Working Theory

Here’s a request that sounds trivial and isn’t: “rename the name column to full_name.” On your laptop, against a table with fifty rows, it’s one line of SQL and you’re done. In production, against a table with fifty million rows being read and written thousands of times a second by code you can’t pause, that same one line can lock the table, stall every request queued behind it, and take the product down for the length of the migration. The gap between those two worlds — the toy table and the live one — is one of the quiet dividing lines between writing code and running a system.

The trap is that a schema change and the code that uses it want to happen at the same instant, and in a live system they can’t. Your database and your running application are two separate things deploying on two separate timelines. For some window, old code and new code are both serving traffic, and old schema and new schema are both in play. If you change the column out from under the running app, the old code — still handling requests — suddenly references a column that no longer exists. Crash.

The way out has a couple of names — expand/contract, or parallel change — and its whole idea is to never make a breaking change in one move. You split the one scary change into a sequence of individually safe ones.

Expand Migrate Contract add new column write to both backfill in batches move reads to new stop writing old then drop it every step is safe alone — running code always has something valid to point at
The one scary change, split into three boring ones. Original diagram · Working Theory

Expand. Add the new thing alongside the old one. Create full_name; leave name exactly where it is. Additive changes are safe because nothing existing depends on the new column yet. Then teach the app to write to both columns on every update — a dual-write — so from this moment on, no fresh data goes stale.

Migrate. Backfill the history: copy each existing row’s name into full_name, in batches, deliberately slowly, so you never lock the whole table at once. Then move reads over to the new column — ideally behind a flag, so you can flip back instantly if something looks wrong. Now the app writes both, reads the new one, and the two columns agree.

Contract. Only once nothing reads or writes the old column — verified, not assumed — do you stop writing it, and, later still, drop it. The scary, irreversible step happens last, when it has finally become boring.

Three phases, and notice what you bought: at no single moment was there a version of the running code pointing at something that didn’t exist. Each step was safe on its own, and safe steps deploy on the app’s normal timeline instead of demanding a synchronized big-bang.

The tax is real and worth stating out loud. For the length of the migration you’re maintaining two columns, writing twice, and carrying a backfill job and probably a feature flag — the system is genuinely more complicated during the change than before or after. That’s the price of not being able to stop the world. The instinct to “just rename it” is the instinct to change two coupled things atomically, and a live system is precisely where you don’t get atomicity across the app/database boundary for free.

There’s a general shape here beyond columns. Splitting one table into two, tightening a constraint, changing a type, dropping a field — the live-system version of nearly every “simple” schema change is the same dance: add the new, move traffic gradually, retire the old, and never leave running code without something valid to point at.

To read more: the expand/contract pattern, also called parallel change — Danilo Sato and Martin Fowler have written the canonical description; the surrounding practices are dual writes, batched backfills, and gating the read cutover behind a feature flag. Distinct from the earlier idempotency, cache-invalidation, backpressure, and observability pieces — this one is about changing state safely while the state is in use.

Liked this? Get the next one in Working Theory.

Going weekly in August (it's in beta now). One genuinely interesting read on building, the brain, and the science most people missed.

Got a reaction, a counter-example, or something I missed? Reply by email — I read everything.
◉ join in

Where have you hit this — in a product you use, or one you're building?

Threads open here soon. For now, the conversation lives two clicks away — discuss on GitHub, or just reply by email. I read and answer everything.