skip to content

Schema Design & Evolution

The judgement half of the contract: naming, mutation shape, nullability and the changes that break a client. A graph has no version number to hide behind, which is why evolution is the harder half.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

Which edits to a GraphQL schema break existing clients, and which do not?

level: juniorimportance: must knowfreq 66%

answer

  1. Ask who exactly is broken
  2. Two failures wear the same word
  3. Does the old document still validate?
  4. Does the old client code still cope?
  5. Removals, renames, new requirements, narrowings

basics

~20 s

Removing or renaming a field, argument, type or enum value breaks clients, as does adding an argument or input field that is non-null with no default. Adding a new nullable field or optional argument breaks nobody.

solid answer

~50 s

Separate two questions. **Does every document that used to validate still validate?** Removing or renaming a field, argument, type or enum value breaks that, as does adding an argument or input-object field whose type is non-null with no default value, or changing a field from a leaf type to an object type. Those failures are loud: validation rejects the whole request before execution, so the caller gets `errors` and no data. **Does client code that used to handle a response still handle it?** That is a different list — adding a value to an output enum leaves every document valid while handing a reader a value it has never seen. In a clinical-trial registry, renaming `Trial.principalInvestigator` breaks every deployed document that selects it even though the data behind it is unchanged. Additive edits — a new nullable field, a new optional argument — break neither list.

code

graphql · 11 lines
graphql
# before
type Trial {
  registryId: ID!
  principalInvestigator: String!
}

# after: both edits invalidate deployed documents
type Trial {
  trialRegistryId: ID!
  leadInvestigator: String!
}

go deeper

for a junior

Be ready to name the obvious breakers on the spot: removing a field, renaming a field, removing an argument or enum value, adding a required argument. Say clearly that a rename is a removal plus an addition, not a cosmetic edit.

for a middle

Explain the mechanism, not just the list: validation runs before execution, so an invalid document fails as a whole request with no data. Be able to sort an edit into 'breaks documents' versus 'breaks client code'.

for a senior

Show that you check breakage against real callers and their deploy cadence, not against the schema in the abstract, and that you know an enum addition can be an incident even though every document stays valid.

for a principal

Own the framing question: breaking is relative to a consumer, so the interesting decision is which parts of the graph get which compatibility regime and who pays when a rule is relaxed.

## "Breaking" is two questions wearing one word Before cataloguing anything, split the failure in two, because the two halves have different victims, different symptoms and different fixes. **Question one: does every document that used to validate still validate?** A GraphQL caller sends an executable document that names fields, arguments and type conditions. The server checks that document against the schema *before* it executes anything. If the schema no longer contains something the document names, the whole request is rejected: the response carries an `errors` entry and no executed data at all. This failure is loud and immediate, and it hits a client that has not changed a line of code, because the client did not have to change — the schema did. **Question two: does client code that used to handle a response still handle it?** Here the document may remain perfectly valid while the *values* coming back are ones the caller has never seen. Nothing on the server looks wrong: validation passes, execution succeeds, latency is normal. The failure lives entirely inside the caller, which is why it is usually found by a user rather than by a dashboard. Nearly every disagreement about whether an edit is "breaking" is really a disagreement about which of these two it trips. ## The edits that break documents Take a clinical-trial registry graph with `Trial`, `Site`, `Participant`, `Enrollment` and `AdverseEvent`. * **Removing a field.** Deleting `Trial.registryId` invalidates every deployed document that selects it. * **Renaming a field.** `Trial.principalInvestigator` becoming `Trial.leadInvestigator` is a removal plus an addition. The data behind it is identical, the old name is gone, and old documents fail. "The data didn't change" is not a defence; the name *is* the contract. * **Removing or renaming an argument.** Turning `trials(phase:)` into `trials(trialPhase:)` breaks every caller that passed the old name. * **Adding a required argument or a required input-object field.** An argument whose type is non-null and which declares no default value must be supplied. Add one to an existing field and every document selecting that field becomes invalid — including documents belonging to teams with no interest in the new argument. * **Changing a field's type incompatibly.** `Site.address: String` becoming `address: Address` breaks documents outright, because an object-typed field requires a sub-selection and a leaf field forbids one. `AdverseEvent.severity: String` becoming an enum keeps documents valid — both are leaf types — but changes what may be *sent*: a quoted `"MILD"` is not a valid enum literal in a document, though the same value passed through a variable still coerces, since enum values travel as JSON strings. * **Removing a type, or removing an interface from a type's `implements` list.** A document carrying a type condition on the vanished name no longer validates, and a caller that branches on the `__typename` meta-field starts seeing a string it does not recognise. * **Removing an enum value.** Every document or variable value that *sends* it is now rejected. * **Widening an output type back to nullable.** `visits: [Visit!]!` becoming `[Visit!]` keeps the document valid but hands readers a null they never had to consider. ## The edits that break code, not documents * **Adding a value to an enum that appears in output position.** Every document still validates; a reader that maps each known value to a behaviour now meets one it cannot map. * **Adding a field to an object type.** Safe for readers — nobody selects it yet. The exception is structural rather than client-facing: a field added to an *interface* must be added to every implementing type, or the schema itself no longer builds. * **Strengthening a nullable output field to non-null.** Safe for readers, who were already coping with null. ## Why GraphQL makes this unusually tractable Two properties help. The schema is a machine-readable artefact, so the difference between two versions is computable rather than a matter of reading release notes. And callers declare their selections up front, so in principle a server can say exactly which documents an edit would invalidate — *if* it has the documents. That conditional is the whole difficulty of a public endpoint that accepts arbitrary documents: the schema owner can enumerate what an edit breaks in theory but not who is doing it. ## The traps Do not confuse "no client uses it" with "no client uses it *today*, on the versions I can see". And do not assume a removal becomes safe because it was announced; announcing a removal changes who is surprised, not what validation does with the old document.

  • Is renaming an object type breaking for a client whose documents never write that type name?
    It can be. The `__typename` meta-field is available on every object, interface and union selection and returns the concrete type name, so any client branching on that string stops matching the moment the type is renamed. Documents that do carry a type condition on the old name fail validation outright. A type rename is never a purely internal edit.
  • Where does a client discover that a field it selects has been removed?
    In validation, which runs before execution. The server rejects the whole request: the response carries an `errors` entry naming the unknown field, and no data is executed at all. There is no partial response and no null placeholder — the other twenty healthy fields in that document do not resolve either.
  • Is adding a field to a type ever a breaking change?
    For readers, essentially never: nobody selects a field that did not exist. The exception is structural. A field added to an interface must also be added to every type implementing it, or the schema no longer builds — so the edit is breaking for the schema author rather than for any caller.

saying these in an interview costs you the question

  • Claims nothing breaks because clients pick their own fields
  • Thinks a removed field comes back as null
  • Calls a rename safe because the data is unchanged
  • Assumes every addition to a schema is safe
  • Believes breakage appears only when a client redeploys
  • Expects partial data when one selected field is invalid

context

open as a page

Why is a GraphQL schema usually designed from client demand rather than from database tables?

level: juniorimportance: must knowfreq 61%

basics

~20 s

The schema is the contract clients hold, not a view of storage. Designing it from the questions clients actually ask keeps types stable when tables change, and stops the shape of the database leaking into the API.

open as a page

In a GraphQL schema, why type a list field's sort argument as an enum rather than a String?

level: juniorimportance: must knowfreq 64%

basics

~20 s

An enum makes the set of sortable keys part of the schema. An unknown value is rejected before execution, introspection publishes the legal values, and no resolver has to defend itself against an arbitrary column name in a string.

open as a page

Why do GraphQL mutations conventionally take a single `input` argument and return a payload type?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Neither half is in the GraphQL specification; both are ecosystem conventions. A single input object gives clients one variable to pass and one place to add fields later. A payload object type leaves room to return more than the record you wrote.

open as a page

Which name rules does the GraphQL specification enforce, and which are only convention?

level: juniorimportance: must knowfreq 54%

basics

~20 s

The specification fixes only the character grammar - a leading letter or underscore, then letters, digits or underscores - case sensitivity, and the reservation of any name starting with two underscores. camelCase fields and PascalCase types are ecosystem habit, not rules.

open as a page

Why is every GraphQL field nullable by default, and what does Non-Null promise?

level: juniorimportance: must knowfreq 78%

basics

~20 s

In GraphQL every declared type is nullable unless it is wrapped in Non-Null, written as a trailing exclamation mark. The specification defaults to nullable so a field that cannot be produced can still return null; Non-Null promises the value is always present.

open as a page

Why expose a related object field in a GraphQL schema instead of a bare foreign-key id?

level: juniorimportance: must knowfreq 74%

basics

~20 s

A field typed as the related object lets one document walk the edge in a single request. A bare id forces the caller into a second request and makes every client hard-code how the two types join.

open as a page

What does a GraphQL schema check compare in CI, and what verdicts does it report?

level: juniorimportance: must knowfreq 56%

basics

~20 s

A schema check diffs the schema your branch would publish against the schema currently published, taken from a registry, and classifies every edit — commonly safe, dangerous or breaking. It is a tooling practice, not part of the GraphQL specification.

open as a page

Why does a GraphQL API usually have no /v2, and what does that demand of every schema change?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Each client names the exact fields it wants, so the server can add types and fields without altering any response already being asked for. Versionless evolution trades a second endpoint for additive change plus deprecation of whatever is retired.

open as a page

In GraphQL, why does adding a non-null argument with no default break existing documents?

level: middleimportance: must knowfreq 51%

basics

~20 s

Validation requires that every argument whose type is non-null and which declares no default value is present in the document. Deployed documents omit the new argument, so they fail validation and the whole request is rejected before execution.

open as a page

Why does a GraphQL schema generated from database tables turn storage refactors into client breakages?

level: middleimportance: must knowfreq 53%

basics

~20 s

Because the generated field names, types and nullability are derived from columns, so renaming, splitting or widening a column produces a different published contract. With no translation layer to absorb the change, storage edits reach clients directly.

open as a page

In a GraphQL partial update, how does the server tell an omitted input field from an explicit null?

level: middleimportance: must knowfreq 66%

basics

~20 s

GraphQL input coercion keeps them apart: an omitted field with no default is absent from the coerced input, while an explicit null is present with the value null. Whether your server surfaces that difference is a mapping question.

open as a page

In GraphQL, why is making an output field Non-Null safe for clients but making it nullable again breaking?

level: middleimportance: must knowfreq 62%

basics

~20 s

Tightening an output field from a type to its Non-Null form only removes a case clients already handle, so nothing they wrote stops working. Widening it back introduces a null those clients never wrote code for, which is why that direction breaks them.

open as a page

Why is an unbounded list field on a GraphQL object type hard to fix later?

level: middleimportance: must knowfreq 63%

basics

~20 s

The field's return type is part of the contract. Adding arguments to it is safe, but replacing a plain list with a paged wrapper type changes what every existing document receives, so the fix is a breaking change to every caller.

open as a page

What does @deprecated(reason:) do in a GraphQL schema, and which locations may carry it?

level: middleimportance: must knowfreq 64%

basics

~20 s

@deprecated is a built-in type-system directive marking a field definition or enum value as retired — and since the October 2021 edition an argument or input field too. It changes nothing at execution; it hides the member from introspection by default.

open as a page

A GraphQL schema check flags a field removal as breaking, yet recorded usage shows zero calls — what would convince you to ship it?

level: seniorimportance: must knowfreq 52%

basics

~20 s

The verdict says a document could select the field, not that one does. Usage evidence overrides it only if the observation window outlives your slowest-updating client, the traffic was not sampled, and the recording covers every path that reaches the schema.

open as a page

In a GraphQL schema, what does a default value on an input object field do when the client omits it?

level: juniorimportance: should knowfreq 48%

basics

~10 s

The server applies the declared default during input coercion, before any resolver runs, so the resolver receives the default value and cannot tell that caller apart from one that sent the same value explicitly.

open as a page

When should a GraphQL list field take one filter input object instead of separate scalar arguments?

level: middleimportance: should knowfreq 54%

basics

~20 s

Use separate arguments for a small, stable predicate set: each is named and deprecable on its own, and one can be genuinely required. Move to a single filter input once the set grows or a sibling field must accept the same predicates.

open as a page

Why name a GraphQL mutation for the business action instead of the record write it performs?

level: middleimportance: should knowfreq 52%

basics

~20 s

A mutation named for the write, such as updateListing, must accept every field change at once, so the schema cannot say which transitions are legal or who may make them. publishListing and withdrawListing carry that intent in the type system.

open as a page

Why are GraphQL field names like getEmployee or payslipsFromHrisV2 discouraged?

level: middleimportance: should knowfreq 48%

basics

~20 s

A field names the data it yields, not the call behind it. The operation type already says the request is a read, so get is noise, and a name carrying the backing system becomes a lie the day that system is replaced.

open as a page

Where does a GraphQL schema check get its baseline, and why keep it in a registry?

level: middleimportance: should knowfreq 43%

basics

~20 s

The baseline is the schema currently published for the environment being deployed to. A registry stores that per environment, with a history of who published what and when, so CI can fetch a pinned, auditable answer instead of guessing from a branch.

open as a page

Why is adding a value to a GraphQL enum risky for readers while removing one breaks senders?

level: seniorimportance: should knowfreq 44%

basics

~10 s

In output position the server picks the value, so a new member reaches readers whose code maps only the values they knew. In input position the client picks, so removing a member rejects senders.

open as a page

How do you keep a demand-oriented GraphQL schema from degenerating into one field per screen?

level: seniorimportance: should knowfreq 43%

basics

~20 s

Treat screens as evidence of domain concepts, not as the units of the schema. Model concepts that several surfaces compose differently, keep root fields as a small set of entry points, and reject any type named after a view.

open as a page

What does a GraphQL mutation that returns only `Boolean` force every caller to do?

level: seniorimportance: should knowfreq 46%

basics

~20 s

It forces a second round trip. The caller holds no updated record, so it must refetch — and that follow-up read is a separate request that can be served stale, showing pre-mutation values the caller believes are fresh.

open as a page

GraphQL has no packages, so how do you keep type names unique across many teams?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Every named type in a schema shares one flat pool - the specification requires unique type names and offers no namespaces. Uniqueness has to come from a shared vocabulary: qualify types by domain concept and keep generic nouns out of the pool.

open as a page

In GraphQL, why is relaxing an argument to nullable safe when relaxing an output field is not?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Because the client writes arguments and reads output fields. Relaxing an argument only widens what the client is allowed to send, so every existing document is still valid; relaxing an output field widens what the client must be prepared to receive.

open as a page

When does a GraphQL schema need a type for the relationship itself rather than a plain list?

level: seniorimportance: should knowfreq 46%

basics

~20 s

When the pairing carries facts of its own — a role, a validity window, an approver — or an identity that mutations must target. Those facts belong to neither end, and break the moment one driver holds two assignments.

open as a page

Your GraphQL schema renamed a field and broke a shipped mobile release — how should that rename have been rolled out?

level: seniorimportance: should knowfreq 52%

basics

~20 s

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

open as a page

How do you set a breaking-change policy for a GraphQL schema with un-updatable clients?

level: principalimportance: should knowfreq 38%

basics

~10 s

Derive the rules from consumer upgrade latency, not from the schema. Segment the graph, freeze only what slow clients reach, publish the client obligations that make additive change safe, and name the escape valve.

open as a page

When would you let teams auto-generate GraphQL schemas from their tables rather than hand-design them?

level: principalimportance: should knowfreq 36%

basics

~20 s

Allow generation where the coupling never has time to hurt: one known consumer, a short-lived or internal surface, a prototype, or a bridge during a migration. Forbid it wherever types are published into a contract or a shared namespace.

open as a page

showing 1–30 of 37