What is Expand-Contract Pattern?
Definition
The pattern has three phases: expand, where a new structure is added alongside the old one without removing anything; migrate, where application code and data transition to use the new structure while the old one remains available as a fallback; and contract, where the old structure is finally removed once nothing depends on it anymore. Each phase is a separate, individually safe deployment rather than one large, risky change. Skipping straight to the end state, renaming a column in a single step, for example, works fine in a system with no live traffic, but breaks immediately in a system where old and new code might both be running during a rolling deployment.
Why the Expand-Contract Pattern matters
A schema change that works perfectly in testing can still cause an outage in production if it doesn't account for the reality that, during a deployment, old and new application code are often running simultaneously against the same database. The Expand-Contract Pattern exists specifically to handle that overlap safely, rather than assuming a change can happen instantaneously and atomically across every running instance of an application at once. It trades a single risky step for several smaller, individually safe ones, which is usually a good trade even though it takes longer overall.
How the Expand-Contract Pattern works
In the expand phase, a change adds new structure, a new column, a new table, without touching or removing anything the old code depends on. During migrate, both old and new code can run against the database safely, since both structures exist; data gets backfilled or transitioned as needed. Only in contract, once every consumer has moved to the new structure and nothing still depends on the old one, does the old structure actually get removed. Each phase can be deployed and verified independently before moving to the next, and the migrate phase can run for as long as it takes every consumer to actually finish transitioning.
How Liquibase helps
Because each phase of Expand-Contract is its own ChangeSet, Liquibase's ChangeLog naturally supports breaking a migration into these distinct, sequential steps rather than requiring a single large script. Preconditions can verify a database is actually ready for the contract phase, confirming no old-structure dependencies remain, before that final, less reversible step runs, adding a safety check the pattern relies on but doesn't automatically enforce on its own. Leaving a comfortable gap between migrate and contract, long enough to be confident nothing unexpected still depends on the old structure, is usually safer than rushing to clean up the old column the moment migration looks complete.
