Legacy and migration

3 min read ·

Expand–Contract: Change Data While Old Code Is Still Running

You need to rename a database field, but older application versions are still running. Why one ALTER TABLE is rarely the whole migration.

The team wants to replace an order-status field with a new representation. In a test environment, renaming a column and updating the queries looks simple. Production is different: older application instances are still running, deployment is gradual and background workers may lag behind the release.

Delete the old field first and old code breaks. Keep only the old model and the new feature cannot move forward. Expand–Contract changes the system in stages so both application generations can coexist safely for a while.

Expand before breaking anything

During expansion, the team introduces the new representation without removing the existing one. A new status column might appear alongside the old column. The application becomes capable of understanding both while consumers begin migrating.

Existing records may need a backfill. That process must account for data volume and concurrent writes while the migration is running. Otherwise the team can finish copying yesterday's values while today's changes remain in the old field alone.

Temporary dual writes or another synchronisation mechanism may be necessary, but the source of truth must be explicit. Without that, values diverge and nobody knows which one to trust.

Coexistence is its own product state

For a period, old and new readers and writers operate together. Test those combinations, not only the newest version. A particularly dangerous case is new code writing a state that old code is still expected to understand.

I would test what happens when an application instance rolls back, how already-migrated records behave and whether a failed data transfer can be repaired. The team should know where rollback is genuinely safe and where another forward migration is required.

The same approach can help change API and event formats with independently deployed consumers, provided their compatibility window and deprecation path are clear.

Contract is not an optional clean-up task

Remove the old form only after establishing that nobody relies on it. “All teams have upgraded” is weaker evidence than observing live consumer versions, requests and usage of deprecated fields.

Then retire temporary synchronisation, old columns and compatibility code. If the final phase is always postponed, the product accumulates two representations and their combinations. A temporary migration becomes a permanent operating cost.

Where simplicity still wins

A local change owned by one team and deployed atomically may not need several stages. But when releases are independent and data is critical, the cost of a single forced cutover can easily exceed the cost of temporary compatibility.

Expand–Contract does not make migration free. It lets the company pay for a controlled transition instead of betting every consumer on one synchronised deployment.

The Prisma migration guide offers one concrete example for changing a database field incrementally.