In a code-first GraphQL server, why can an internal rename break a working client?
answer
- The public name has an internal owner
- Refactoring tools rename both halves at once
- Nothing in the diff mentions the API
- A removed field is a rejected document
- Explicit names plus a printed artefact
basics
~20 sBecause the published field names are derived from server-side identifiers. A refactor that renames a property renames the public field with it, and the rename looks like ordinary internal cleanup in review — no API artefact changed for anyone to notice.
solid answer
~40 sCode-first ties each schema name to an identifier in the server's source unless you state the schema name explicitly. So a rename refactor is simultaneously an internal change and a **contract change**, and the refactoring tool applies it silently and correctly across the codebase — including across the public surface. In a donations graph, renaming a `grossAmountCents` property to `amountMinorUnits` removes `Donation.grossAmountCents` from the schema; a reconciliation client still selecting it now fails **validation**, so the whole operation is rejected before execution rather than degrading field by field. Two defences matter: declare schema names explicitly on exposed members so the public name survives internal churn, and print the assembled schema to a committed file so a rename shows up as a surface diff in review instead of as a tidy-up.
code
pseudocode · 9 lines// derived: the schema field name follows the identifier
objectType(Donation) {
expose(grossAmountCents) // -> Donation.grossAmountCents
}
// decoupled: the public name is a literal a rename cannot touch
objectType(Donation, schemaName = "Donation") {
expose(amountMinorUnits, schemaName = "grossAmountCents")
}go deeper
Remember that in code-first the field names clients use come from names in the server code, so renaming a property can rename part of the API. If you touch something exposed in the graph, say so in the pull request.
Explain the derivation and its consequence: a rename removes one field and adds another, and a client whose document names the removed one fails validation outright. Know that most libraries let you pin the schema name explicitly.
Be able to describe the incident end to end — how it merged, why review missed it, what the client actually received — and the two defences you would put in place. Add the safe rename sequence: add, deprecate, migrate, remove.
Own the policy. Decide whether exposed names must be explicit strings in every service on the graph, whether printing the schema is mandatory in the build, and how much refactoring freedom a team gives up in exchange for a contract that survives its own cleanups.
## The mechanism: derived names In a code-first server the schema is assembled from declarations in the server's own language, and by default the schema's names come from the language's names. A class becomes an object type named after the class; a property or method becomes a field named after it, usually with a casing convention applied. That default is most of what makes code-first pleasant — you declare a thing once and it appears in the graph. It is also the trap. **Every internal identifier that feeds a name is now part of the public contract**, and nothing in the source says so. The property is just a property. Renaming it is the safest, most encouraged operation in software, the one your tools perform for you with confidence, and here it is a breaking API change wearing the costume of a cleanup. ## What it looks like in practice A charity donations graph exposes: ```graphql type Donation { id: ID! grossAmountCents: Int! receivedAt: String! } ``` A reviewer looking at the finance service notices the codebase uses three different words for money and standardises on minor units. The property `grossAmountCents` becomes `amountMinorUnits`. Tests pass — they were written against the same identifier and were renamed with it. The diff reads as pure hygiene: no SDL file in the repository, no contract file, nothing that says *interface*. The deployed schema now has `Donation.amountMinorUnits` and no `grossAmountCents`. The nightly reconciliation job, whose document has been pinned to `grossAmountCents` since it was written, walks the donations in pages of 8,400 rows. Its next run sends the same document it has sent for a year and gets back a **request error**: the document references a field that does not exist on `Donation`, so it fails validation and the server never executes it. There is no partial result, no null, no per-field error to catch — the operation is rejected whole. A client that would have tolerated a null gets nothing. ## Why review did not catch it This is the deeper point, and the one an interviewer is usually fishing for. In schema-first, that change is impossible to make invisibly: you would have had to edit the SDL, and the SDL diff says `- grossAmountCents` `+ amountMinorUnits` in the language of the API. In code-first, if no schema artefact is committed, there is nothing in the pull request that speaks about the API at all. The reviewer is not careless; they were shown a rename and they approved a rename. The same pull toward the internal side shows up in smaller ways all the time: extracting an enum value's name, splitting a class in two, moving a computed value from a property to a method with a different name, changing a parameter name so an argument is renamed. Each is a routine refactor and each edits the contract. ## The two defences **Decouple the names.** Essentially every code-first server library lets you state a schema name explicitly rather than inheriting the identifier. Doing that on everything exposed — types, fields, arguments, enum values — is the structural fix: the public name becomes a deliberate string that a refactor cannot touch, and changing it becomes an obvious edit to a literal that says "this is the name clients see". The cost is ceremony on every declaration, which is exactly why teams skip it and exactly why this incident is common. **Give the contract a file.** Print the assembled schema on every build and commit the result. Now the rename produces a diff in an SDL file, the pull request shows a removed field, and the conversation happens before the merge rather than after the nightly job fails. It costs a build step and the discipline of regenerating, and it converts an invisible change into a visible one — which is all you need, because a visible removal will be argued about. Those two are complementary rather than alternative: explicit names prevent the accident, the printed artefact catches whatever the explicit names did not cover. ## What a good answer adds Be honest that the reverse tradeoff exists. Schema-first makes this rename impossible to do accidentally, but it makes it *tedious* to do deliberately, and it introduces its own class of defect where the SDL and the code disagree. Neither approach removes the underlying problem, which is that a public name is a promise and a codebase does not know which of its identifiers are promises. What removes the problem is making the surface reviewable — by writing it, or by printing it — so that the change is discussed as an API change rather than discovered as an outage. And note the asymmetry that makes removals worse than additions: a removed field is not a degraded response, it is a rejected document. Anything that widens the schema is absorbed by existing clients; anything that narrows it is felt immediately by every client whose document mentions the narrowed part.
- Why is a removed field worse for the caller than a field that starts returning null?A null is a value the response can carry, so the rest of the document still executes and the client can decide what to do. A field that no longer exists makes the document invalid, so the server rejects it before execution and returns no data at all. Additive changes degrade; subtractive ones are all-or-nothing.
- If you must rename a public field, how do you do it without an outage?Add the new field alongside the old one, mark the old one deprecated so it is visible as retiring while it still works, move consumers over, and only then remove it. The rename becomes an addition followed much later by a removal — which is the only sequence that never leaves a client sending an invalid document.
- Does explicitly naming every exposed member make code-first equivalent to schema-first?No. It fixes the accidental-rename problem, but the contract is still scattered across declarations rather than gathered in one readable document, so nobody can review the surface as a whole. That is why teams that pin names still print the schema — the two defences solve different halves of the problem.
saying these in an interview costs you the question
- Treating a rename as purely internal in code-first
- Expecting a removed field to return null instead
- Assuming a partial response when validation fails
- Relying on code review to spot a derived-name change
- Believing type-safe server code implies a stable contract
- Renaming in place instead of add-deprecate-remove