The Expand and Contract Pattern
Changing a database schema is one of the scariest things you can do in a live system. If you drop or rename a column while old servers are still running, those servers crash trying to reference something that no longer exists. The alternative — a single coordinated deployment that changes everything at once — is a high-wire act that gets more dangerous as the system grows.
The Expand and Contract pattern solves this across four deliberate steps.
Expand: Add the new column without removing the old one. Update your code to write to both, but still read from the old one — the new column is a shadow write, accumulating data without yet being trusted as the source of truth:
// Phase 1: Expand (write to both, read from old)
async function saveUser(user: User) {
await db.query("UPDATE users SET name = ?", [user.fullName]);
await db.query("UPDATE users SET fullname = ?", [user.fullName]);
}
Migrate: Run a background script to copy all existing data from name to fullname. This backfills the rows that were written before the shadow write existed, so the new column is complete before anything depends on it.
Switch: Update your code to read from fullname. The old column stays in place to support any servers still running the previous deployment — but it's no longer the source of truth:
// Phase 3: Switch (write to both, read from new)
async function getUser(id: string) {
const row = await db.query("SELECT fullname FROM users WHERE id = ?", [id]);
return row.fullname;
}
Contract: Once everything is stable and no code references name, delete it.
It requires more deploys and more coordination than a single migration. The payoff is a schema change that no running server ever notices.