Phased Refactoring: The Expand-Migrate-Contract Pattern

Executing seamless database schema transitions by decoupling structural changes from data migration and application updates.

Database refactoring often presents a significant risk to high-availability systems. When a schema change requires both structural updates and data migration, a traditional "stop-the-world" approach leads to unacceptable downtime. The Expand-Migrate-Contract pattern (also known as Parallel Change) solves this by splitting a single breaking change into multiple additive and non-breaking phases.

This methodology ensures that the database remains compatible with current application versions throughout the entire process. By avoiding massive, all-or-nothing deployments, engineering teams can verify each step, roll back if necessary, and maintain a consistent experience for downstream systems and end-users alike.

Phase 1: The Expand Phase

The first step involves adding the new structure to the database without removing or altering the existing one. For example, if we are splitting a large table into two smaller ones, we create the new tables and relationships alongside the original. During this phase, no application logic changes for reads, but we might start preparing the environment for dual operations.

  • Introduce new columns or tables with proper constraints.
  • Maintain existing triggers, views, or stored procedures to ensure backward compatibility.
  • Avoid deleting any legacy fields until the entire cycle is complete.

Phase 2: The Migrate Phase

Once the new structure is in place, we begin the transition. This is the most critical stage, as it involves keeping the old and new data synchronized. We update the application to "dual-write"—saving data to both the old and new structures simultaneously. This ensures that any new records are available in both formats.

While dual-writing handles new data, we perform a background migration (backfilling) for historical records. This process moves older rows from the legacy structure to the new one in small batches to avoid locking tables or saturating database performance. We then switch the application to read from the new structure, verifying that the results match expectations.

Phase 3: The Contract Phase

The final phase begins only after we are 100% confident that the new structure is the system of record and that no applications or reporting tools are still accessing the old schema. We remove the dual-write logic from the code, effectively making the new structure the sole destination for data.

After a cooling-off period where we monitor logs for any unexpected legacy access, we safely drop the old columns or tables. This contracts the database back to its optimized state, completing the refactoring without a single minute of downtime. It is a disciplined approach that prioritizes reliability over speed, ensuring that complex schema evolution remains a routine engineering task rather than a high-stakes emergency.

Comments & Discussion

This could be your first comment. Share your thoughts on phased refactoring below.

Leave a message