Evolutionfield removaldeprecationmigration windowtelemetry

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.

▶ Run the labFollow the failure

Frame the contract

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

Design question
What has to be true — and measured — before a field that consumers once read can safely disappear?
Consumers
Everyone still reading the field, which is the whole problem: the consumers you know, the dashboard someone built in 2024, and the partner script whose author no longer works there. Removal safety is exactly as good as your knowledge of this set.
The promise
A removal process guarantees that when the field disappears, verifiably nobody is reading it — and everyone who was had a replacement, a guide, and a deadline they heard about through a channel they actually watch.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

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.

The lifecycle, with the clocks that make it real
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 date

The 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.

yesnoShip replacementMeasure old-field readsDeprecate visiblyMigration window + nudgesReads = 0?RemoveContact remaining readers
UserLLMAgentToolDataDecisionHumanGuardrail

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.

The census that precedes most removal outages
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
7ship the removal Friday
The census that is actually evidence
1Read telemetry, `user.legacy_score`, last 30d:
2 key_partner_a 0 reads (migrated w/ ticket #812)
3 key_partner_b 0 reads
4 key_bi_tool 1.2k reads/daystill reading
5 mobilev4.1 ~12% of fleet, field rendered
6
7NOT removable. Chase bi_tool owner;
8 mobile needs the fleet2% or an
9 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.

Compatibility Analyzer
Eight proposed changes to a shipped API. Which break existing clients?
Add optional response field `nickname`
Rename response field `status` → `state`
Change `amount` from number to string ("19.99")
Server starts returning new enum value `suspended`
New optional request parameter `sort`
Make optional request field `currency` required
Return 422 instead of 400 for validation failures
Add a new endpoint `GET /projects/{id}/activity`

Follow the failure

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

  1. 1
    Team → API: wants to rename legacy_score to trust_score; treats it as a cleanup, not a removal.
  2. 2
    Team → evidence: greps its own repos, checks the docs, finds nothing — concludes nobody uses the old field.
  3. 3
    Team → deploy: ships the rename in place; the old key disappears from every response in one release.
  4. 4
    Consumers → production: a partner's BI dashboards go blank; an old mobile fleet renders empty profile sections; neither appeared in any grep.
  5. 5
    Team → 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.
What breaks
  • Invisible readers break at once and diagnose slowly: a missing field often fails as undefined propagating 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.

Design the contract
  • • 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.
Observe in production
  • • 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.
Evolve without breaking
  • • 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]]).
What it costs
  • • 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.

Misconceptions

Claim
“We searched all our code — nobody uses the field.”
Reality
Your code is where the writers live; the readers live in other repos, BI tools, spreadsheets and partner scripts you cannot grep. Only read-side telemetry, or a request-side send-count, is evidence about readers.
Claim
“It has been marked deprecated for a year, so removing it is fair.”
Reality
Deprecation is notice, not migration. If telemetry says a consumer still reads the field daily, removal is a chosen breakage regardless of how long the label has been there — the year proves your channel did not reach them, which is your problem to fix.
Claim
“Renaming a field is a smaller change than removing one.”
Reality
A rename is a removal (old name) plus an addition (new name), and readers of the old name break identically. The pleasant-sounding word is how renames skip the process that removals would have gotten.

Apply it