Your GraphQL schema renamed a field and broke a shipped mobile release — how should that rename have been rolled out?
answer
- The whole operation failed, not one field
- Expand, migrate, contract
- Both names, one source of truth
- Aliases cannot rescue a missing field
- The slowest client sets the date
basics
~20 sNever rename in place. Add the new field beside the old one, resolve both from the same source, mark the old one @deprecated with a pointer to its replacement, and delete it only once no live client still selects it.
solid answer
~50 sRenaming `Statement.closingBalance` to `endingBalance` in one commit removes a field, and selecting a field a type does not define is a **validation** failure, not a field error. That means the whole operation is rejected before execution: the response carries `errors` and no `data` entry at all, so the entire statement screen goes blank rather than one number going missing. A shipped mobile binary cannot be patched, so 61% of installs were broken until users updated. The safe rollout is expand–migrate–contract. **Expand**: add `endingBalance` alongside `closingBalance`, both resolving from the same underlying value so they cannot drift. **Migrate**: mark the old field `@deprecated(reason: "Use endingBalance.")` so tooling and generated types steer new work to the replacement, and let clients move on their own release cadence. **Contract**: remove the old field only when evidence shows nothing selects it. The removal date is set by the slowest client you cannot force to upgrade, not by the server team's convenience.
code
graphql · 8 linesscalar Money
type Statement {
id: ID!
endingBalance: Money!
closingBalance: Money!
@deprecated(reason: "Renamed; use endingBalance. Removal after 2026-11-30.")
}go deeper
Remember the sequence: add the new field, deprecate the old, remove much later. Know that selecting a field the schema no longer defines fails the whole operation rather than nulling one value.
Explain why removal is a request error with no data entry in the response, and why both spellings must be backed by one resolved value so they cannot drift during the migration window.
Show incident judgement: identify the blast radius from the document, not the field; separate cosmetic renames from semantic changes; and treat removal as gated, scheduled work with usage evidence behind it.
Own the policy question underneath — who decides how long a dying field lives, what the org promises client teams, and how you keep a graph from accumulating permanent duplicate spellings nobody dares delete.
## Why a rename hurts more than it looks like it should Intuition says removing one field costs you one field. In GraphQL it costs you every document that mentioned it. Selecting a field that is not defined on the type is a **validation error** — a request error, raised before execution begins. The specification is explicit that when a request fails before execution, the response does not carry a `data` entry at all. So the client does not get its other eleven fields with one hole in them; it gets an error list and nothing else. In the incident that prompts this question, the statements screen of mobile build 4.9 sent a single document selecting twelve fields on `Statement`, one of them `closingBalance`. The rename deployed at 09:14 and the screen was blank for every user on 4.9 — 61% of installs that morning — with roughly 4,800 rejected operations in the first eighteen minutes. No amount of client-side cleverness helps: an alias renames the response key, not the schema field, so clients cannot alias their way around a field that no longer exists. And a shipped binary cannot be redeployed; the only fix available to the server team was to put the old field back. ```json { "errors": [ { "message": "Field 'closingBalance' is not defined by type 'Statement'.", "locations": [{ "line": 4, "column": 5 }] } ] } ``` (The exact message text is up to the implementation; what is specified is that this is a request error and that `data` is absent.) ## Expand: add before you subtract The pattern is the same expand–migrate–contract shape used for database columns, applied to a schema. Add `endingBalance` next to `closingBalance` and ship that alone. It is a pure addition: no deployed document selects the new name, so nothing changes for anyone. Now the graph can answer to both names for as long as it needs to. The important detail is that both fields must resolve from **one source of truth**. Two resolvers computing "the balance" independently will drift the first time somebody fixes rounding on one of them, and then you have two fields with the same name-ish meaning and different values — a far worse failure than the rename, because it is silent. ```pseudocode resolve Statement.endingBalance(statement): return statement.balances.closing # the deprecated spelling delegates - no second code path, no drift resolve Statement.closingBalance(statement): return resolve Statement.endingBalance(statement) ``` ## Migrate: make the mark do work Mark the old field with `@deprecated` and put something useful in the reason. "No longer supported" — the directive's default — tells a client engineer nothing. `@deprecated(reason: "Renamed; use endingBalance. Removal after 2026-11-30.")` names the replacement and the deadline, and that string is what shows up in an explorer, in generated types and in editor tooltips. Deprecated members also drop out of the default introspection listing, so a new screen written next month is unlikely to pick the dying name by accident. What the mark does not do is anything at runtime. Every existing document keeps working unchanged, which is exactly the property you want: the deprecation is an announcement, not an outage. ## Contract: removal is a separate, evidenced piece of work Removal is where the discipline actually lives, and it needs two inputs. First, evidence that no live client selects the field — which is a matter of recorded field usage over a window long enough to cover infrequent screens, and is its own topic. Second, a deliberate check of the proposed schema against the published one before it merges, which is likewise its own topic. The point to make in an interview is that removal is scheduled work with a gate in front of it, not an opportunistic tidy-up while you are in the file. ## When the rename is not really a rename Distinguish two cases, because they get very different treatment. A **cosmetic** rename — same value, better name — has real cost and modest benefit: you carry two spellings for months, generated types get noisier, and the 37-field `Statement` briefly becomes a 38-field one. It is sometimes still worth it, but be honest that clarity is the whole return. A **semantic** change dressed as a rename is the opposite: if `endingBalance` means something different from what `closingBalance` meant, then the new name is doing essential work, because keeping the old name and quietly changing its meaning would break every consumer silently, with nothing to diff and nothing to validate. In that case the deprecation reason should say what changed, not just where to go. The rule that falls out of both: in a versionless graph, additions are cheap, removals are scheduled, and in-place edits to something a client already names are the one move that has no safe form.
- Could the mobile client have aliased its way out of the rename?No. An alias renames the key in the response, not the field in the schema — `closingBalance: endingBalance` still requires `endingBalance` to exist in the document, which the shipped build does not contain, and `closingBalance` as written no longer resolves to anything. Aliases help a client keep its own response shape stable while migrating forward; they cannot conjure a field the server has deleted.
- Why is the old field's resolver written to delegate rather than duplicated?Because two independent implementations of the same value drift. The first rounding fix, timezone correction or cache change applied to one and not the other produces two fields that disagree, and no test will notice because both are individually plausible. Delegating keeps a single source of truth for the whole deprecation window, which may run for months, and makes the eventual deletion a one-line removal.
- What would you do if the old field had to be removed before clients could migrate?Degrade before you delete. If the field is nullable, having it return null keeps every document valid and leaves one blank value instead of a blank screen; if it is non-null, that null would propagate, so the field must first be relaxed to nullable. That buys a smaller failure than a validation error, and it is a last resort for legal or security-forced removals, not a normal migration path.
saying these in an interview costs you the question
- Thinks a removed field just returns null
- Renames in place and tells clients to update
- Believes an alias can cover a deleted field
- Copies the resolver instead of delegating
- Removes the deprecated field on the next sprint tidy-up
- Sets the removal date from the server team's calendar