How do you set a schema deprecation policy for a GraphQL API whose oldest clients are shipped mobile apps?
answer
- Advisory by default, organisational in practice
- Classify clients by upgrade control
- The install-age curve sets the window
- A mark needs an owner and a date
- Unfinished removals are the real failure
basics
~20 sMake deprecation a promise, not a label: a stated window, a named replacement, an owner and a date. The window comes from how long the oldest client you cannot force to upgrade survives in the field.
solid answer
~50 sA versionless graph has one schema serving every consumer at once, so `@deprecated` is the only carve-out mechanism you have — and on its own it does nothing. A workable policy has four parts. **Classify consumers** by whether you control their upgrade: internal callers move in weeks, a web bundle in days, a shipped mobile binary only as fast as its install base drains. **Derive the window from that drain curve** per class, rather than picking a round number. **Turn each mark into a commitment**: a reason naming the replacement, an owner, a target date, and a gate that checks live usage before anything is deleted. **Fund the contraction**, because the failure nobody plans for is a schema where deprecation means nothing — a 37-field type carrying eleven tombstones that new engineers cannot read. Name the tradeoff out loud: every extra week of window is dual maintenance, every week cut short is somebody's broken app.
code
graphql · 10 linesscalar Money
type Statement {
id: ID!
endingBalance: Money!
closingBalance: Money!
@deprecated(
reason: "Renamed; use endingBalance. Removal after 2026-11-30. Owner: statements team."
)
}go deeper
You will not own this, but know the shape: deprecation is a promise with a date, and a field stays alive until the clients that use it are gone. Do not delete a marked field because it looks unused.
Be able to explain why the removal date is a property of the client population rather than the server roadmap, and what a good reason string must contain for a client engineer to act on it.
Show you can run the process: classify consumers by upgrade control, gate removals on usage evidence, and argue for the contraction work that turns a mark into an actual deletion.
Own the whole tradeoff — dual-maintenance cost against broken customers, what the org promises client teams in writing, when a parallel type family beats field-by-field migration, and how you stop a single schema from filling with tombstones nobody will delete.
## Why this is a policy problem and not an engineering one In a versioned API you can freeze a shape and let it rot in place; the old contract keeps its own address and its own lifetime. A versionless graph has no such compartment. One schema serves the internal reporting job, the web app, the partner integration and a mobile build from fourteen months ago, simultaneously. `@deprecated` is the only carve-out mechanism the specification gives you, and it is purely advisory — nothing stops a client from selecting a deprecated field, and nothing forces you to ever remove one. Everything that makes deprecation mean something is organisational. ## Start by classifying consumers by upgrade control The useful axis is not "internal versus external" but **can I cause this client to change?** - **Callers you deploy**: batch jobs, other services, server-rendered pages. Days to weeks, and you can often do the migration for them. - **Callers a user reloads**: a web bundle. Hours to days, bounded by cache and long-lived sessions. - **Callers a user installs**: shipped mobile binaries. Months, with a long tail you do not control, and a floor set by however many people never update at all. On a statements graph, the third class is the one that sets policy. If build 4.9 is on 61% of installs the morning you deprecate, the question is not "how long is reasonable" but "what does the install-age curve look like at the 99th percentile, and where does it flatten". That number is measurable, and it is the honest input to the window. ## Turn the mark into a commitment A deprecation that says only `@deprecated(reason: "No longer supported")` is a shrug. A deprecation that is a commitment carries four things: the **replacement** (what do I write instead), the **date** (when does this stop resolving), an **owner** (who is accountable for the removal, tracked wherever the team tracks work), and a **gate** (what evidence must exist before the field is deleted). The evidence and the mechanical check are separate disciplines — recorded field usage over a window long enough to cover monthly and quarterly screens, and a diff of the proposed schema against the published one in the pipeline — but the policy is what says they are required. The date belongs in the reason string as well as in the tracker, because the reason string is the only part of the migration that reaches a client engineer reading a generated type at 11pm. ## Fund the contraction, or deprecation stops meaning anything The failure mode that shows up in real graphs is not premature removal; it is removal that never happens. Marks accumulate. A `Statement` type reaches 37 fields, eleven of them deprecated, four of them alternative spellings of the same money value. The costs are diffuse and real: generated client types carry the dead members, every explorer session needs a second look with `includeDeprecated: true` to understand history, onboarding engineers cannot tell which of three balance fields is canonical, and each retained field is still a live, resolvable, supportable path with its own backend calls and its own authorization surface. So the policy needs a contraction budget — a standing slot for finishing removals — and a rule that a new deprecation without a date is not accepted. Otherwise the org has quietly chosen the worst combination available: no version boundary and no cleanup. ## Levers when the window is not affordable Sometimes waiting out the install base is not an option — a field leaks data it should not, or a regulator requires it gone. The levers, roughly in order of preference: - **Minimum supported client**: refuse very old builds at the transport and prompt an upgrade. This is a blunt, visible break, but it is a break you schedule and communicate rather than one that surprises you. - **Degrade before deleting**: make the field nullable and return null. Old documents stay valid, one value goes blank instead of a whole screen. It buys a smaller failure, not a free one, and it only works while nothing non-null depends on it. - **Behaviour keyed to the caller**: possible, and mostly a trap. It re-introduces per-client contracts inside a single schema, which is precisely the thing one graph was supposed to eliminate, and it makes the schema stop describing what a client actually gets. ## When a real version is the honest answer Occasionally the change is not a field but a whole domain model, and dripping it out field-by-field would take years. The lower-cost form of that is a **parallel type family inside the same schema** — new types, new root fields, old ones deprecated en masse — rather than a second endpoint. You keep one registry, one set of tooling, one authorization model and one operational surface, and you accept a temporary, dated duplication. A second endpoint doubles the operational and governance story and should be reserved for the case where the two graphs genuinely serve different audiences. ## What to say out loud in the interview Name the tradeoff rather than the number. Every additional week of deprecation window is a week of dual maintenance and dual truth; every week you cut is somebody's broken app. A strict policy fails loudly and rarely, a lax one fails quietly and permanently, and the job of the policy is to make that choice deliberate and visible rather than an accident of who was on call.
- How do you pick the actual length of a deprecation window?From measurement, not convention. Look at how the install base of the oldest client class ages: what share is still on a build older than the deprecation after four, eight and sixteen weeks, and where the curve flattens. The window is where the residual traffic is small enough that a scheduled, communicated break is cheaper than another quarter of dual maintenance. Publish the number so client teams can plan against it.
- What do you do about a deprecated field that has sat in the schema for three years?Treat it as a policy failure rather than a field problem. The immediate move is to check whether anything still selects it and remove it if not. The systemic move is to stop accepting deprecations without a date and an owner, and to fund the contraction work, because once a graph contains tombstones nobody dares delete, the mark has stopped carrying information and client teams learn to ignore it.
- Would you ever cut a second versioned endpoint for a GraphQL API?Rarely, and only for a whole-domain redesign that cannot be dripped out field by field. Even then the cheaper form is a parallel family of types and root fields inside one schema, with the old family deprecated together: one registry, one toolchain, one authorization model, one operational surface. A separate endpoint doubles all of those and is justified mainly when the two graphs serve genuinely different audiences.
saying these in an interview costs you the question
- Picks a round window with no usage data behind it
- Treats @deprecated as enforcement rather than a signal
- Never schedules removals, only new deprecations
- Assumes all clients can be upgraded on request
- Reaches for a second endpoint as the first answer
- Keeps duplicate field spellings indefinitely to be safe