Database Migration Compatibility (Expand-and-Contract)
Database migration compatibility coordinates schema and application changes so old and new application versions can safely operate against the same database during deployment. The core technique is to make schema changes backward- and forward-compatible, then remove obsolete structures only after all consumers have migrated.
A deployment can temporarily run multiple application versions because instances are replaced gradually, rolled back, or deployed using blue-green or canary techniques. Therefore, a schema change must not assume that every application instance starts using the new schema simultaneously. Compatibility means each intermediate database state works with every application version that may legitimately access it.
The common approach is Expand-and-Contract. During the expand phase, add new structures without breaking existing codeāfor example, add a nullable column or a new table while retaining the old column. Deploy application code that can work with both representations, migrate data if necessary, and gradually switch reads and writes to the new structure. Only after the old application version is no longer able to access the database should the contract phase remove or constrain obsolete structures.
For a Java backend, this often requires separating schema deployment from application deployment. A migration tool such as Flyway or Liquibase can apply versioned changes, while the application rollout is designed around compatibility boundaries. Destructive operations such as dropping a column, renaming a column in place, or changing a type incompatibly should generally be deferred until no running application version depends on the old representation.
Compatibility also applies to data semantics, not just SQL structure. For example, changing an enum value, making a nullable field mandatory, or changing the meaning of an existing value can break an older binary even when the database accepts the operation. Safe migrations therefore consider reads, writes, constraints, indexes, data backfills, transaction behavior, rollback paths, and the period during which multiple application versions coexist.
Key points
- Design every intermediate schema to support all application versions that may be running.
- Use Expand-and-Contract: add compatible structures first, migrate consumers and data, then remove legacy structures.
- Avoid destructive schema operations in the same deployment that introduces code depending on the new schema.
- Treat data semantics, constraints, defaults, and enum/value changes as compatibility concerns, not merely DDL concerns.
- Backfills should be planned separately from latency-sensitive deployment steps when they can create significant database load.
- A rollback plan must account for both application binaries and the database state.
Interview questions
How would you safely rename a database column used by a Java service?
I would avoid a direct rename if old application versions may still run. First, add the new column while retaining the old one, then deploy code capable of reading or writing both as needed. Backfill existing records and switch all writers and readers to the new column, verifying that old consumers are gone. Finally, remove the old column in a later migration.
Why should database schema changes be compatible with multiple application versions during deployment?
Deployments are often gradual, so old and new application instances can run at the same time. If the new schema immediately removes something the old binary expects, those old instances can fail. Compatibility keeps the shared database usable throughout the rollout. It also makes rollback safer because the previous application version can continue operating.
Explain the Expand-and-Contract migration pattern.
Expand first by adding the new schema elements while preserving the old representation. Deploy application code that understands the transition and gradually migrate reads, writes, and existing data. Once all relevant consumers use the new representation, contract by removing obsolete columns, tables, or constraints. The important property is that each intermediate state remains compatible with the application versions that can access it.
What makes a database migration unsafe even when the SQL statement succeeds?
Successful DDL does not prove that all application behavior remains compatible. A constraint can reject writes from an older version, a changed enum value can break deserialization, or a backfill can overload the database. Distributed consumers such as workers and scheduled jobs can also continue using the old contract after the main service has been deployed. I therefore evaluate the migration against application versions, data semantics, operational load, and rollback requirements rather than only checking whether the SQL executes.
How should rollback be designed when a deployment includes a database migration?
I first distinguish application rollback from database rollback because they are not necessarily symmetric. Additive schema changes are valuable because the old application can continue using the retained structures while the deployment is reverted. Destructive changes should generally be postponed until rollback of the previous application version is no longer required. For irreversible data transformations, I would use backups, dual representations, or a forward-fix strategy rather than assuming the DDL can simply be reversed.