Expand-and-Contract Migration
Expand-and-contract migration separates a database change into backward-compatible additions and a later cleanup phase. This allows different application versions to coexist during rolling or zero-downtime deployments.
The expand phase adds new schema elements without breaking code that still uses the old schema. For example, a new nullable column can be introduced while existing application instances continue reading and writing the original column.
The application is then changed in a compatibility-aware sequence. A common pattern is to deploy code that can understand both the old and new representations, backfill existing data, and gradually switch reads or writes to the new representation.
Once all application instances use the new schema and old data paths are no longer needed, the contract phase removes obsolete columns, indexes, tables, or compatibility code. This cleanup is deliberately separated from the initial deployment so rollback and mixed-version operation remain possible.
For a Java backend, this pattern is especially useful when multiple application instances are deployed incrementally. The database must remain compatible with both the version being replaced and the version being introduced throughout the transition.
Key points
- Make the expansion backward-compatible with existing application versions.
- Deploy application compatibility before removing the old schema path.
- Backfill data separately when a new representation needs historical values.
- Switch reads and writes deliberately rather than changing everything atomically.
- Remove legacy schema only after all consumers have migrated.
Interview questions
How do you handle a migration where a new field must be populated for millions of existing rows?
I would separate schema creation from the historical-data backfill. The new field would initially permit the state required by existing rows, and the backfill would run in controlled batches to limit locks, transaction size, and database load. The application should not depend on the backfill being instantaneous. After validation, I could enforce stronger constraints and remove the old representation in a later migration.
What can go wrong with dual writes during an expand-and-contract migration?
The two writes can diverge if one succeeds and the other fails, particularly when they involve separate operations or systems. Retries can also create ordering or idempotency problems. I would define the consistency requirement, make operations retry-safe where possible, monitor discrepancies, and have a reconciliation strategy. Depending on the database and migration, a transactional approach, change-data capture, or a carefully designed single-write path may be preferable.
What problem does expand-and-contract migration solve?
It allows a database schema to evolve while old and new application versions may be running at the same time. The initial expansion is designed to be backward-compatible, so an older instance does not fail when the new schema exists. After the application has migrated, the obsolete schema is removed in a separate cleanup step. This reduces deployment coupling and makes rolling deployments safer.
How would you rename a database column safely?
I would avoid directly renaming the column while old application instances still depend on the original name. First I would add the replacement column, deploy code that can work with the transition, and populate the new column from existing data. I would then move reads and writes to the new column and verify that old consumers are gone. Only afterward would I remove the original column.
How would you design an expand-and-contract migration for a zero-downtime Java service?
I would first identify every reader and writer of the affected schema and define compatibility requirements between deployed versions. I would expand the schema, deploy application code that can operate across the transition, perform any required backfill, and then progressively switch reads or writes. I would verify correctness and operational metrics before removing the legacy path. Rollback considerations are important because the database must remain compatible with the application version to which traffic may be reverted.