Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Parallel Change

Pattern

A reusable solution you can apply to your work.

“Whenever I have to make a contract change in one of these situations, I find I can break down my work into three phases: expand, migrate, contract.” — Martin Fowler

Change an interface by adding the new form first, migrating callers across at their own pace, and removing the old form last, so consumers never see a breaking change.

Also known as: Expand-Contract

Understand This First

  • Contract – a parallel change is a disciplined way to evolve a contract without breaking it.
  • Interface – the interface is what you expand and later contract.
  • Migration – the middle phase is a migration of consumers from the old form to the new one.

Context

Most software does not live alone. Your function is called by other functions. Your database table is queried by other services. Your API has clients you do not control. The moment a consumer depends on the shape of something you own, any change to that shape becomes a coordination problem.

You cannot always stop the world to ship a change. Even when you own every caller, you may not want to. A single big-bang rename across a large codebase is risky, hard to review, and impossible to roll back cleanly. When the callers belong to other teams, other companies, or other agents, big-bang is off the table entirely.

Problem

You need to change something other code depends on: a function signature, a column name, a JSON field, an API endpoint, a configuration key. The new design is better. The old design has callers you cannot upgrade atomically. How do you get from one to the other without a broken window in the middle?

Forces

  • Callers cannot all change at the same instant, especially across teams, services, or versions.
  • A breaking change is expensive to recover from, because every downstream failure has to be diagnosed and patched under pressure.
  • Deferring the change indefinitely leaves the old, worse design in place and accumulates new callers that deepen the problem.
  • Running two designs side by side costs clarity: the code now describes both the past and the future at once.
  • Rollback must stay cheap at every step, because any step can reveal a problem you did not anticipate.

Solution

Expand the interface to hold both the old and new forms, migrate every caller from old to new, then contract the interface to remove the old form. Each phase is a separate change that ships on its own. At no point is there a breaking change, because the old form keeps working until the last caller has moved off it.

The three phases:

Expand. Add the new form alongside the old one without removing anything. If you are renaming a column, add the new column and write to both. If you are renaming a function parameter, accept both the old and new names. If you are replacing an endpoint, serve both the old and new paths. The system now has two ways to do the same thing, and both produce the same result.

Migrate. Move callers from the old form to the new one, one at a time. Each migration is a small, reviewable change. If the callers are code you own, you edit them directly. If the callers belong to other teams, you announce the new form, mark the old form as deprecated, and wait. If the callers include external partners or paying customers, you give them a sunset date that is long enough to be fair.

Contract. Once nothing reads or writes the old form, remove it. This is the only step that actually deletes code. By the time you get here, deletion is safe because the old form has no callers. If you are unsure whether anyone still depends on it, you are not done migrating.

The reason the pattern works is that it separates the shape change from the caller changes. A breaking change rolls both together. Parallel change pulls them apart so each can proceed at its own pace and be rolled back independently.

How It Plays Out

A payments team needs to rename a database column from amount to amount_cents to make the unit explicit. The old column stores dollars as a floating-point number, which has been causing rounding bugs. Rather than rename in place and break every query, the team ships three pull requests over two weeks. The first adds amount_cents as an integer column and backfills it from amount; application code writes to both columns. The second moves each read and write site across to amount_cents, one service at a time. The third drops the amount column once a dashboard confirms nothing has read from it in seven days. No deploy ever broke the payments path. Any individual step could have been reverted without reverting the others.

A platform team maintains an internal API consumed by twenty services across eight teams. They want to replace a boolean is_active field with an enum status that has four values. They add status to the response, compute it from is_active for now, and document that is_active is deprecated and will be removed in ninety days. A dashboard tracks which services still read the old field. Each team migrates on their own schedule. After ninety days the platform team checks the dashboard, confirms the old field is unused, and removes it in a final cleanup. The coordination cost of a big-bang change (twenty simultaneous pull requests, twenty review cycles, one shared maintenance window) never happened.

Tip

When directing an agent through a parallel change, describe all three phases as separate tasks in your plan. Agents that try to rename something in a single shot will edit the definition and every caller in one diff, which is exactly the big-bang change you are trying to avoid. Ask for the expand step, verify it lands cleanly, then ask for the migrate step, then ask for the contract step.

An agentic team uses the pattern to rename a function used in hundreds of places. They ask an agent to add a new function with the new name that delegates to the old one. The agent ships that in a single commit. Next they ask the agent to rewrite every call site to use the new name, one directory at a time, running tests after each batch. When the old name has no remaining callers, confirmed by a quick grep, they ask the agent to delete the old function. The work that would have been a single terrifying diff becomes a sequence of boring, verifiable steps.

Consequences

Benefits. Every step is independently deployable and independently reversible. There is no moment where the system is partially upgraded and broken. Callers move at their own pace, which matters when they are owned by different teams. Rollback is cheap because each phase is small and the old form stays available until the contract step. The pattern works for code, database schemas, APIs, configuration, and message formats alike. It is the same technique at every level.

Liabilities. The code is temporarily more complicated. Two forms exist in parallel, and anyone reading the code has to understand which one to use. Tests and documentation have to cover both forms during the middle phase. If you skip the contract phase or forget it, the two forms live together forever, and new callers pick whichever one they see first. Forgotten parallel changes are a common source of technical debt: the expand shipped, the migrate happened, the contract never did.

The middle phase also takes real time. If external consumers are involved, the deprecation window may last months. You cannot hurry a parallel change by skipping the wait, because the whole point is to give callers time.

  • Depends on: Contract – parallel change is the canonical way to evolve a contract without breaking it.
  • Depends on: Interface – the interface holds both forms during the middle phase.
  • Uses: Migration – the migrate phase is a migration of callers from the old form to the new one.
  • Uses: Feature Flag – a flag can toggle between the old and new form during the migration phase, giving you a per-request rollback.
  • Enables: Strangler Fig – large-scale strangler migrations rely on parallel change at the interface level to keep consumers working while the implementation moves underneath.
  • Enables: Refactor – parallel change is refactoring applied to a public interface, where you cannot rely on atomic code updates.
  • Mitigates: Technical Debt – parallel change is one of the few low-risk techniques for removing debt from a system in active use.
  • Contrasts with: Rollback – rollback recovers from a failed change after it ships; parallel change is the discipline that prevents needing to roll back in the first place.
  • Related: Continuous Delivery – parallel change makes breaking interfaces compatible with always-deployable trunks.
  • Related: API – public APIs are the most common place the pattern is applied, because their callers are outside your control.

Sources

Martin Fowler named and formalized the Parallel Change pattern in a 2014 bliki entry, drawing together practices already in use for safe schema migrations and API evolution. His three-phase structure (expand, migrate, contract) became the canonical framing.

Danilo Sato and Martin Fowler’s follow-up writing on evolutionary database design, particularly in Refactoring Databases (Scott Ambler and Pramod Sadalage, 2006), developed the same technique for schema changes: add the new column, dual-write, backfill, cut over reads, drop the old column. The database case is the clearest instance of the pattern and the one that most teams encounter first.

Sam Newman’s Building Microservices (2015, second edition 2021) extends parallel change to service-to-service contracts, showing how expand-contract interacts with consumer-driven contract testing and deprecation lifecycles across team boundaries.

The broader principle, that risky changes should be split into small, independently reversible steps, runs through Kent Beck’s Extreme Programming Explained and the continuous delivery literature from Jez Humble and David Farley. Parallel Change is one of the most-cited concrete techniques for living up to that principle at the interface level.