Evolutionmigrationdual supportexpand-contractcompatibility matrix

API Migration: Running the Change End to End

Every breaking change, whatever its label, runs the same program: ship the new surface, support both, move consumers with telemetry and deadlines, deprecate, remove. The compatibility matrix — which client works against which API — is the map; the burn-down is the engine.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
How does every consumer get from the old contract to the new one without an outage on either side of the change?
Consumers
Both sides of the seam: consumers who must schedule migration work into roadmaps they already committed, and the provider team that will operate two surfaces — and answer for both — until the last consumer moves.
The promise
A run migration guarantees continuity: at every moment between announcement and removal, every consumer is on a supported path, knows its deadline, and can verify its own progress — and the old surface dies on a date, not by surprise.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

One program, whatever the change is called

A field rename (Removing Fields Without Removing Consumers), an endpoint redesign, a version sunset (Versioning: What a Version Even Promises), an auth scheme change — the labels differ, but any change that old clients cannot absorb runs the same five-stage program: expand (ship the new surface beside the old), dual-support (both work, verifiably), migrate (consumers move, tracked per consumer), deprecate (the old surface gets its campaign — see Deprecation as a Process, Not a Label), contract (remove, on evidence). This is expand-migrate-contract, the same maneuver databases use for schema changes, applied at the contract boundary.

The defining property is that *the provider never breaks both surfaces at once and never breaks either without warning*. The naive alternative — the cutover, where old is replaced by new in one deploy — requires every consumer to change at the same instant as the provider, which is only possible when you deploy the consumers too. That is worth saying positively: for an internal API where one team owns both sides and can deploy them together, a coordinated cutover is *cheaper* than the program, and running the full five stages there is ceremony (see Public vs Internal APIs). The program exists for every case where consumers move on their own schedules — which is every public API and most internal ones past the third consumer.

guide + deadlinetelemetrytail remainsyesExpand: ship new surfaceDual supportMigrate consumersBurn-down = 0?Deprecate + brownoutsContract: remove old
UserLLMAgentToolDataDecisionHumanGuardrail

The compatibility matrix is the map

Before the first stage ships, write the compatibility matrix: every consumer class down the side, every provider state across the top, and in each cell — works, works with caveats, or breaks. The matrix is where the plan's lies surface early. "Mobile v4 works against the dual-support API" — does it, when the dual API returns the new error format on the shared error path? "The v1 adapter serves old clients" — including the pagination behavior they depend on, or just the happy path? Every cell marked *works* is a claim that needs a test; the cells everyone forgets are the diagonal ones, where an old client meets the new surface accidentally — through a load balancer misroute, a copied URL, or an SDK that upgraded underneath its user.

Dual support has two honest implementations. Two real surfaces: old and new endpoints both served natively — simple to reason about, doubles the code paths to test and patch. Adapter: old surface reimplemented as a translation onto the new one (in the service or at the gateway — see The Gateway as Policy Boundary) — one source of truth, but the adapter must reproduce old *behavior*, not just old shapes: default page sizes, error formats, ordering quirks that consumers depend on (Backward Compatibility: The Real Rules's semantic clauses, again). Adapters rot silently, so the old surface's contract tests must keep running against the adapter for as long as it lives — the adapter is now the thing making the old promise.

A real compatibility matrix, mid-migration
ConsumerAPI v1 only (past)Dual support (now)v2 only (target)
Partner on v1 SDKworksworks — v1 served by adapter; error format pinnedbreaks — hard blocker until migrated
Web app (migrated)worksworks on v2works
Mobile ≥ v5.0worksworks on v2works
Mobile ≤ v4.1 (12% fleet)worksworks on v1 adapterbreaks — gate: fleet < 2% or forced upgrade
Quarterly billing batchworksworks on v1 — untested against adapter until next rununknown — the cell that pages you in Q3

Moving consumers is the actual work

Teams budget the engineering (new surface, adapter, tests) and starve the part that determines the timeline: making other people do work on your schedule. Migration speed is set by consumer effort, so the provider's highest-leverage spend is reducing that effort: a migration guide with exact old→new mappings, SDK releases where upgrading the dependency *is* most of the migration, dual-read verification endpoints (call new, compare with old, report drift), and per-consumer progress visibility — "your key made 14,000 v1 calls last week, from these 3 code paths" turns a vague obligation into a to-do list (see Consumer-Driven Evolution: Telemetry Before Breakage).

And someone must own the finish. A migration with no owner reaches 80% and stalls forever — the provider now runs two surfaces indefinitely, which is the worst outcome: all the cost of the program, none of the payoff. The owner drives the burn-down through the deprecation endgame (Deprecation as a Process, Not a Label's enforce/extend/exempt/cancel decision), and only then does the contract stage ship. A useful rule of thumb for sizing: the consumer-facing half of a migration — guides, comms, chasing, support — costs as much as the engineering half, and for public APIs with long tails, more.

The cutover, sold as a migration
1Plan:
2 1. build /v2 (6 weeks) ← funded
3 2. "announce migration"an email
4 3. turn off /v1 (one deploy) ← scheduled
5
6# no matrix, no adapter, no burn-down,
7# no owner for consumer progress.
8# Step 3 slips 3× then ships anyway;
9# the quarterly batch job finds out in Q3.
The program, costed honestly
1Plan:
2 1. expand: /v2 + v1 adapter, contract
3 tests pinned on BOTH surfaces
4 2. matrix: every consumer × every state,
5 each "works" cell has a test
6 3. migrate: guide + SDK release +
7 per-key progress dashboards
8 owner: A. Chenburn-down weekly
9 4. deprecate: headers, deadline,
10 brownouts at T-30d
11 5. contract: remove when burn-down = 0
12 or remainder dispositioned

The left plan budgets only the code. The right plan budgets the map (matrix), the engine (burn-down + owner) and the endgame — which is where migrations actually succeed or die.

Key points

  • Every breaking change runs the same program: expand → dual-support → migrate → deprecate → contract; never break both surfaces at once, never break either by surprise.
  • The coordinated cutover is legitimate exactly when one team can deploy both sides together — and ceremony everywhere else.
  • Write the compatibility matrix first: every consumer × every provider state, and every "works" cell is a claim that needs a test.
  • Adapters make dual support cheap and rot silently — the old surface's contract tests must run against the adapter for its whole life.
  • Migration speed is set by consumer effort: guides, SDK upgrades, drift-check endpoints and per-key progress reports are the highest-leverage spend.
  • An unowned migration stalls at 80% and leaves you running two surfaces forever — all of the cost, none of the payoff.

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Team → plan: budgets six weeks to build /v2; the migration itself is a bullet point reading "communicate to consumers".
  2. 2
    Team → deploy: /v2 ships; /v1 is declared legacy the same day, with no adapter contract tests and no per-consumer tracking.
  3. 3
    Consumers → roadmaps: attentive ones schedule migration next quarter; the tail schedules nothing, because nothing forces the question.
  4. 4
    Team → deadline: the v1 shutdown slips twice (someone big is not ready), then ships on the third date to "finally force the issue".
  5. 5
    Consumers → production: the quarterly batch job and two partner integrations break; the postmortem finds no matrix row, no burn-down, and no owner who could have named them beforehand.
What breaks
  • Consumers on the untracked tail take hard outages at contract time — and they are found by the outage, not by the plan.
  • Stalled migrations double the provider's permanent surface: two contracts to test, patch, document and answer for, with the team's velocity mortgaged indefinitely.
  • Slipped-then-enforced deadlines damage the org's migration credibility, making every subsequent program slower — consumers rationally wait out announcements that historically slipped.

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Fund the program, not the feature: matrix, adapter tests, guide, telemetry, owner and endgame are in the plan and costed before the new surface is built.
  • • Keep the old surface's contract tests green against whatever now serves it — native code or adapter — until the day it is removed.
  • • Reduce consumer effort mechanically: old→new mapping tables, an SDK release that carries most of the change, and a drift-check path for consumers to verify their own migration.
  • • Name the owner and the gate: a single accountable person drives the burn-down, and the removal ships on evidence (zero, or named-and-dispositioned) — never on calendar pressure alone.
Observe in production
  • • The per-consumer burn-down of old-surface traffic is the program's single source of truth; the matrix's "works" cells map to contract tests whose results are the second.
  • • Watch adapter drift explicitly: sampled dual-execution (run both paths, diff results) catches the adapter quietly diverging from old behavior before consumers do.
  • • Track consumers appearing on the old surface *after* deprecation — new integrations against dying surfaces mean your discovery paths (docs, examples, SDK defaults) still point backward.
Evolve without breaking
  • • Each migration leaves rails for the next: matrix templates, adapter patterns, brownout tooling and burn-down dashboards are reusable — the org's migration muscle is real infrastructure.
  • • Design new surfaces to be migration-friendly from birth: per-consumer credentials, version signals in traffic, and contract tests make the *next* change trackable by default (see [[consumer-driven-evolution]]).
  • • Batch compatible breaks into one program where consumers overlap — one migration asking for three changes beats three migrations, because the consumer's fixed cost (scheduling, testing, release) dominates.
What it costs
  • • Dual support is paid daily: two surfaces (or a surface and its adapter) to test, patch, monitor and reason about, for the entire window — long windows are kindness to consumers and tax on the provider.
  • • The program's rigor has a floor cost that small changes struggle to justify; batching helps, and the internal-cutover shortcut exists for exactly this reason — use it where its precondition genuinely holds.
  • • Per-consumer progress visibility requires the attribution infrastructure of [[consumer-driven-evolution]]; without it the program still works, but the tail is found by outreach and luck instead of queries.

Misconceptions

Claim
“Once v2 ships, the migration is mostly done.”
Reality
Once v2 ships, the migration has not started. The work is moving consumers — guides, SDKs, telemetry, chasing, the endgame — and it reliably costs as much as building the surface did, or more.
Claim
“Dual support means running the old code alongside the new.”
Reality
That is one implementation. The other — an adapter translating the old surface onto the new — halves the sources of truth but must reproduce old *behavior* (defaults, error shapes, ordering), and needs the old contract tests running against it for life.
Claim
“Setting an aggressive shutdown date makes consumers move faster.”
Reality
A date consumers do not believe moves nobody — and dates slip when the matrix has unknown cells. Credible dates come from burn-downs and dispositioned tails; aggression without evidence produces slips, and slips teach consumers to wait.

Apply it