Safeguard
Developer Tools•

Expand and contract migrations: keep database changes reversible

Why dropping a column in the same release as the code change blocks rollback, and how expand and contract migrations keep every step reversible.

Safeguard Security Team
4 min read

An expand and contract migration changes a database in separate releases: add the new structure, run old and new code side by side, move to the new structure, and only later remove the old one. Each step can be rolled back, because the previous application version still works against the data.

The problem it solves

Say a release renames a column and ships the code that uses the new name. If the migration runs as part of the deploy, the old column is gone the moment it finishes. If you now roll the code back, the old code looks for a column that no longer exists.

Rollback is one of your main safety tools, so a migration that disables it quietly raises your risk. It is also a security issue. When a flaw is found in the new release, a fast rollback is often your first response, and a blocked rollback means a longer window of exposure.

This post is general engineering practice rather than a summary of one published source, so test it against your own database and tooling.

The three phases

Expand. Add the new column, table or index. Do not remove or rename anything. The old application version is unaffected, because everything it needs is still there. This release is safe to roll back.

Migrate. Deploy code that writes to both old and new structures, or reads from the new one with a fallback. Backfill existing rows in batches. During this period old and new code both run correctly. Rolling back the application is still safe.

Contract. Once nothing reads the old structure and you have confirmed it, remove it in a later release. This is the step that cannot be undone, so take a backup and wait a full release cycle first.

A worked example

Goal: split a single full name column into first and last name columns.

  1. Release one adds first and last name columns, nullable. Nothing uses them.
  2. Release two writes all three columns on every save and reads the new ones when present.
  3. A backfill job fills the new columns for old rows, in small batches to avoid long locks.
  4. Release three reads only the new columns.
  5. After a quiet period, release four drops the old column.

At each point you can go back one step.

Things that bite

  • Locks. Some changes lock a table. Test on realistic volume and learn how your database engine handles each statement.
  • Defaults and constraints. Adding a not null constraint before backfilling fails or blocks writes.
  • Data written by the new version. After rollback the old code must tolerate rows the new code created.
  • Dropped data is gone. Contract steps need backups you have actually restored from.

Security angle

Permissions and encryption changes follow the same pattern. Do not remove access for the old path until the new one is verified. When you migrate to a new hashing or encryption scheme, support both for a time, re-encode as users sign in or in a controlled job, and retire the old scheme last.

For the application side, Safeguard's SBOM and dependency scanning shows what each version contains, which helps when deciding which version to return to. The free scan is at gold.safeguard.sh/scan.

Quick check

Can every migration be made reversible?

No. Destructive steps cannot be undone. The point is to isolate them into a late, deliberate step with a backup.

How long should I wait before the contract step?

Long enough that you would no longer roll back to a version needing the old structure. Often that means a full release cycle.

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.