Removing Fields Without Removing Consumers
Addition is a deploy; removal is a program. Introduce the replacement, measure who still reads the old field, deprecate it visibly, run a real migration window, and remove only when telemetry — not hope — says zero. The steps are boring; skipping any of them is an outage.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
Why removal is the hardest change in the book
Evolution is asymmetric. Adding a field risks nothing when readers are tolerant (see Backward Compatibility: The Real Rules); removing one breaks every reader, and *readers are invisible by default*. Nothing in an HTTP exchange tells you which response fields the client actually used — the payload went out whole, and what happened after parsing happened on someone else's machine. That asymmetry is the entire discipline: additions are cheap and reversible, removals are expensive and irreversible, so the contract accretes unless removal has a real process.
The temptation is always the shortcut: "we grepped the monorepo", "the docs never mentioned it", "it has been deprecated for a year". Each is a proxy for the only question that matters — *is anything still reading it?* — and each proxy fails in a characteristic way: the grep misses the other repo and the BI tool; undocumented fields get discovered and used anyway (Hyrum's Law — see What an API Contract Actually Is); and a deprecation nobody was forced to notice has notified nobody. Renames deserve a special warning: a rename *is* a removal plus an addition, and gets the full process. There is no "just renaming" a field consumers read.
t+0 ADD replacement field ships alongside the old one
(both populated, values consistent, docs point to new)
t+0 MEASURE per-consumer read telemetry on the old field begins
t+1mo DEPRECATE old field marked in docs, spec, SDK (@deprecated),
response headers; changelog + direct notice to readers
t+1mo… WINDOW migration window — public APIs: 6–18 months;
internal: weeks, driven by PRs not patience
NUDGE telemetry-targeted reminders; brownouts near the end
t+N REMOVE only when reads = 0, or every remaining reader is
individually known, contacted, and accepted the dateThe five steps, and what each one is actually for
Introduce the replacement first. The old field must have somewhere for its readers to go before you ask them to move. Ship the new field beside the old with both populated and consistent — this is expand-migrate-contract applied to a payload. If the replacement changes meaning or units, that consistency window is also where you catch your own translation bugs, while both values are visible side by side.
Measure, then deprecate, then window. Measurement has to precede deprecation, because the telemetry baseline tells you whether the migration is moving at all (see Consumer-Driven Evolution: Telemetry Before Breakage for the instrumentation). Deprecation is a *broadcast* act: docs, the machine-readable spec, generated SDKs (@deprecated puts a strikethrough in every consumer's IDE — the single highest-conversion channel you have), and response metadata for the consumers who read neither (see Deprecation as a Process, Not a Label). The window's length is set by your slowest consumer class: a fleet of 18-month mobile builds needs a window longer than any internal service does.
Remove on evidence. The finish line is a telemetry query returning zero — or a short, named list of remaining readers who have each been contacted and either migrated, accepted the date, or been consciously overruled by someone with the authority to accept the breakage. "Probably nobody" is not a state this process recognizes. And removal should still be reversible for a beat: keep the server able to re-emit the field for a release or two, because the reader you missed will surface within days of removal, and re-adding a field is cheap precisely because addition is the safe direction.
Knowing who reads a field: the measurement problem
Response fields are the hard case: the server sends everything and learns nothing about what was read. You have three levers, in increasing order of fidelity. Proxy signals — SDK versions (a consumer on an SDK released after the replacement shipped *probably* uses the new field), and which endpoints each key calls. Field selection — if the API supports sparse fieldsets (?fields=) or is GraphQL, reads are directly attributable per field per consumer; this measurement dividend is a real argument for field selection on large resources (see GraphQL: Client-Shaped Queries Over One Schema and Over-Fetching and Under-Fetching). SDK read telemetry — instrumented accessors that report deprecated-field reads, the most precise and the most invasive.
Request fields are mercifully easy — the server sees exactly who still *sends* the old field, per key, per version — which is why removal plans should split the two directions rather than treating "the field" as one thing. Whatever the levers give you, the output must be a per-consumer burn-down: not "usage is down 80%" but "these five keys, owned by these three companies, still read it" — because the last 20% is where the entire risk lives, and percentages do not have email addresses.
1Evidence for removing `user.legacy_score`:2 - grepped our repos: no references ✓3 - not in the public docs since 2024 ✓4 - deprecated in changelog 6 months ago ✓5 - traffic to /users unchanged (so what?)6 7→ ship the removal Friday1Read telemetry, `user.legacy_score`, last 30d:2 key_partner_a 0 reads (migrated w/ ticket #812)3 key_partner_b 0 reads4 key_bi_tool 1.2k reads/day ← still reading5 mobile ≤ v4.1 ~12% of fleet, field rendered6 7→ NOT removable. Chase bi_tool owner;8 mobile needs the fleet ≤ 2% or an9 accepted-breakage sign-off.The left column proves the team stopped *writing* references; only the right column knows about *readers*. The BI tool and the old mobile fleet appear in no grep — they appear only in telemetry, which is why measurement is a step and not a nicety.
Key points
- Evolution is asymmetric: additions are cheap and reversible, removals break invisible readers — so removal is a program, never a deploy.
- The sequence is fixed: ship the replacement → measure reads → deprecate visibly → run a real window → remove on evidence.
- A rename is a removal plus an addition and gets the full process; there is no "just renaming" a read field.
- Response-field readership is invisible by default — buy visibility with SDK versions, field selection, or instrumented SDKs; request fields the server can measure directly.
- The finish line is a per-consumer burn-down at zero, or a named list of contacted stragglers — percentages do not have email addresses.
- Keep the removal reversible for a release or two; the reader you missed surfaces within days, and re-adding is the cheap direction.
Compatibility Analyzer
Change the contract and observe which guarantee moves.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Team → API: wants to rename
legacy_scoretotrust_score; treats it as a cleanup, not a removal. - 2Team → evidence: greps its own repos, checks the docs, finds nothing — concludes nobody uses the old field.
- 3Team → deploy: ships the rename in place; the old key disappears from every response in one release.
- 4Consumers → production: a partner's BI dashboards go blank; an old mobile fleet renders empty profile sections; neither appeared in any grep.
- 5Team → rollback: re-adds the field under pressure, now maintains both names indefinitely with no plan — the accretion the process was meant to prevent, plus an incident.
- Invisible readers break at once and diagnose slowly: a missing field often fails as
undefinedpropagating through consumer code, far from the parse site. - Consumer trust in deprecation notices dies if a removal ever lands early or unannounced — the next migration window gets ignored, rationally.
- Aborted removals leave dual fields forever: every future reader must learn which of two names is real, and the contract's surface grows monotonically.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Ship the replacement before deprecating the original, with a both-populated consistency window that doubles as validation of the new field's values.
- • Deprecate through channels consumers cannot miss: docs, spec annotations that generated SDKs turn into IDE strikethroughs, response headers, and direct contact for known heavy readers (see [[deprecation]]).
- • Instrument readership before the window opens — SDK versions and key-level attribution at minimum, field-selection or SDK read telemetry where the resource justifies it.
- • Define the removal gate numerically in the plan (reads = 0, or named-and-contacted list) and keep re-emission possible for one or two releases after removal.
- • The per-consumer read burn-down is the program's primary dashboard; a plateau means your nudges stopped converting and the deadline is fiction.
- • After removal, watch consumer-side error rates and support tickets for `undefined`/missing-key symptoms — the missed reader announces itself within days if you are looking.
- • Track deprecated-field count and age across the whole API: a monotonically growing list means removals are being started and never finished.
- • Each completed removal is also a process test: the telemetry, channels and gate you built are reusable for every future field, and the second removal costs half the first.
- • Batch removals into themed cleanup versions or dated releases where possible, so consumers absorb one migration effort instead of a dribble of tiny ones.
- • Fields designed with removal in mind — behind sparse fieldsets, in SDKs with read telemetry — make the next decade of this work cheap; that is a design-time gift to your future self (see [[response-contracts]]).
- • The process is slow by construction: a field can take a year to remove, and the dual-field window is real ongoing cost — two names, one truth, consistency checks between them.
- • Read telemetry has a privacy and complexity price (instrumented SDKs, field-level logging), and proxy signals under-count exactly the oldest, riskiest consumers.
- • Sometimes the honest business decision is to never remove — the field costs less to keep than the migration costs to run; making that call explicitly beats a zombie deprecation.