Expand and Contract
The hard part of most breaking changes is that the old shape is still in use while you’re trying to introduce the new one.
Expand and contract, sometimes called parallel change, is the pattern I reach for when I need to change an interface or schema without taking the system down. Instead of replacing the old thing in one shot, you add the new thing beside it, move callers over in stages, then remove the old one.
Expand
The expand phase adds the new shape next to the old one. For an API, that might mean a new field or endpoint. For a database, it usually means adding nullable columns or a new table instead of changing the existing structure in place.
Here’s a small example. Let’s say users.name needs to become users.first_name and users.last_name.
The new columns get added first. The application still reads from the old column, but writes go to both.
flowchart LR
App["<strong>Application</strong>"] -->|read| Old[("<strong>Old</strong><br/><span class='mermaid-detail'>users.name</span>")]
App -->|write| Old
App -->|write| New[("<strong>New</strong><br/><span class='mermaid-detail'>users.first_name<br/>users.last_name</span>")]
Users don’t see any difference, and the new columns start filling up with live data.
Migrate
This phase takes the longest. You backfill existing rows and move callers over gradually. Feature flags help because they let you switch writes and reads at different times. Dual writes can start early, and reads can wait until you’re confident.
In our example, a backfill job splits users.name for existing rows while the application keeps writing to both places. Once you’ve checked that the new columns are correct, you flip reads over to them.
flowchart LR
Backfill["<strong>Backfill job</strong>"] -->|copy and split| New[("<strong>New</strong><br/><span class='mermaid-detail'>users.first_name<br/>users.last_name</span>")]
App["<strong>Application</strong>"] -->|read| Old[("<strong>Old</strong><br/><span class='mermaid-detail'>users.name</span>")]
App -->|dual write| Old
App -->|dual write| New
The order can vary. Sometimes you dual-write and then backfill, and sometimes you backfill before updating clients. Either way, every step in between needs to be deployable and backward compatible.
Teams tend to underestimate this phase. You often need code that doesn’t belong in the final design, like adapters or compatibility endpoints whose only job is to connect the old shape to the new one. This is called transitional architecture: code you write to make the change safe and delete once it’s done. A strangler fig migration leans on the same idea at a larger scale.
Rollback deserves some thought too. Early on, backing out is easy. Once the new path stores data the old one can’t represent, rolling back gets expensive and can mean losing that data. That’s fine, but make it a deliberate decision.
Contract
Once every caller reads and writes through the new path, you can drop the old one. Remove the legacy column, delete the compatibility code, and the system is back to a single shape.
flowchart LR
App["<strong>Application</strong>"] -->|read and write| New[("<strong>New</strong><br/><span class='mermaid-detail'>users.first_name<br/>users.last_name</span>")]
Cleanup["<strong>Cleanup</strong>"] -->|drop| Old[("<strong>Old</strong><br/><span class='mermaid-detail'>users.name</span>")]
It sounds obvious, but this is the step that gets skipped most. The cutover finishes, the old path stays around for “just a bit longer”, and the team ends up maintaining two shapes forever. If you never contract, you’ve only made things more complicated.
Is it worth it?
Writing code you know you’ll delete can feel wasteful. In my experience it pays for itself. Releases are smaller and easier to roll back, and parts of the new system can ship before the migration is finished, so you get feedback earlier.
What matters most is the habit of breaking one risky change into a series of small, reversible ones. Each phase should be releasable on its own, and every piece of temporary code should have an owner and a plan for removing it.