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.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
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.
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.
| Consumer | API v1 only (past) | Dual support (now) | v2 only (target) |
|---|---|---|---|
| Partner on v1 SDK | works | works — v1 served by adapter; error format pinned | breaks — hard blocker until migrated |
| Web app (migrated) | works | works on v2 | works |
| Mobile ≥ v5.0 | works | works on v2 | works |
| Mobile ≤ v4.1 (12% fleet) | works | works on v1 adapter | breaks — gate: fleet < 2% or forced upgrade |
| Quarterly billing batch | works | works on v1 — untested against adapter until next run | unknown — 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.
1Plan:2 1. build /v2 (6 weeks) ← funded3 2. "announce migration" ← an email4 3. turn off /v1 (one deploy) ← scheduled5 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.1Plan:2 1. expand: /v2 + v1 adapter, contract3 tests pinned on BOTH surfaces4 2. matrix: every consumer × every state,5 each "works" cell has a test6 3. migrate: guide + SDK release +7 per-key progress dashboards8 owner: A. Chen — burn-down weekly9 4. deprecate: headers, deadline,10 brownouts at T-30d11 5. contract: remove when burn-down = 012 or remainder dispositionedThe 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.
- 1Team → plan: budgets six weeks to build /v2; the migration itself is a bullet point reading "communicate to consumers".
- 2Team → deploy: /v2 ships; /v1 is declared legacy the same day, with no adapter contract tests and no per-consumer tracking.
- 3Consumers → roadmaps: attentive ones schedule migration next quarter; the tail schedules nothing, because nothing forces the question.
- 4Team → deadline: the v1 shutdown slips twice (someone big is not ready), then ships on the third date to "finally force the issue".
- 5Consumers → 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.
- 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.
- • 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.
- • 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.
- • 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.
- • 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.