skip to content

In schema-first GraphQL, how do the SDL file and the resolver code drift apart?

level: middleimportance: should knowfreq 50%

answer

  1. Two files, no compiler between them
  2. The binding is a lookup by name
  3. A missing implementation is often silent
  4. The mirror case is simply dead code
  5. Strictness is a server choice, not specified

basics

~20 s

They are two artefacts nothing checks jointly. SDL can declare a field no code implements, code can implement a field the SDL never declares, and either side can be edited without the other. Most drift surfaces only at runtime.

solid answer

~50 s

Schema-first keeps the contract in SDL and the implementation in code, and the link between them is a lookup by name performed when the server builds its schema. Nothing in the compiler relates the two, so four drifts are common: a field declared in SDL with no implementation, which falls to default resolution and typically yields **null**; an implementation for a field the SDL does not declare, which is simply never called; an argument renamed on one side only, so the resolver reads a value that is never supplied; and a type or nullability change in SDL that the code does not honour, which fails when a real value is coerced. Some servers validate the binding when they assemble the schema and refuse to start on an unmatched name; many do not, and the strictness is a server choice, not a specified rule.

code

graphql · 7 lines
graphql
type Campaign {
  id: ID!
  title: String!
  raisedMinorUnits: Int!
  giftAidReclaimedMinorUnits: Int
  donations(limit: Int, after: String): DonationConnection!
}

go deeper

for a junior

Know that in schema-first the SDL and the code are separate files linked by name, and that adding a field to the SDL does not make it work. If you add a field, add its implementation and a test that selects it in the same change.

for a middle

Be able to walk through each drift and say what a client observes: a silent null, dead code that never runs, an argument the resolver never receives, or a coercion failure on real data. Say plainly that strict wiring is a server behaviour, not a specified rule.

for a senior

Show how you close the gap operationally: strict assembly where the server offers it, tests that execute against the built schema rather than parsing the SDL, and review that refuses a one-sided change. Be able to say which drift bit you and how you found it.

for a principal

Weigh this class of defect against the cost of the alternative. Code-first removes all four drifts by construction; the question you own is whether that saving outweighs losing a contract artefact humans wrote and reviewed, and what you put in the build to compensate for whichever side you pick.

## Two artefacts, one unchecked relationship Schema-first buys you a contract you can read. What it costs you is a second relationship that no single tool owns: the SDL text says a `Campaign` has a `raisedMinorUnits` field, and somewhere in the server there is code meant to produce that value. The connection between them is made **by name**, at the moment the server assembles its executable schema. The type checker for your server language never sees the SDL, and the SDL parser never sees your code. That is the whole source of drift. Both files are editable, both are reviewable, and neither is invalidated by a change to the other. ## The four drifts, in the order teams hit them **1. SDL declares a field nobody implements.** Someone adds `Campaign.giftAidReclaimedMinorUnits: Int` to the schema ahead of the implementation, or in a merge that lost the code half. The schema is perfectly valid — a field with no custom implementation is legal, and the server falls back to default resolution, which looks for a matching property on the parent value. If the parent object has no such property, the field resolves to **null**. The client sees a field that exists, is selectable, is documented and is always empty. Nothing logs an error, because nothing went wrong from the server's point of view. If the field had been declared Non-Null, the same situation is loud instead of silent: a null where the schema promised a value is a field error, and the failure surfaces on the first request that selects it. **2. Code implements a field the SDL does not declare.** The mirror image, and it is dead code. The field cannot be selected — a document naming it fails validation — and the implementation is never invoked. It usually survives for months because nothing complains about it. **3. An argument is renamed or retyped on one side only.** The SDL says `donations(first: Int, after: String)` and someone renames the SDL argument to `limit` while the code still reads `first`. The document now validates, the server executes, and the resolver reads an argument that was never supplied — reaching for a default page size instead of the client's. This one is nasty precisely because it produces plausible wrong behaviour rather than an error: a client asking for 25 donations quietly receives the default. **4. Nullability and type changes the code does not honour.** SDL is where nullability lives in schema-first, so tightening `String` to `String!` is a one-character edit in a file the implementation never reads. The code keeps returning null for the rows that have no value, and the change fails on real data — usually not in a test fixture, which is exactly why it reaches an environment. ## When each is caught There is no specified answer here, and that is worth saying plainly in an interview. The specification defines type-system validity — a schema whose interfaces are not properly implemented, or whose argument is typed as an output type, is invalid and must be rejected — but the *binding* between an SDL field and a piece of server code is outside the specification entirely, because the specification does not know your server has code. So strictness is a server-library choice, and libraries differ. Some assemble the schema strictly: every declared field must have either an implementation or a matching property, every implementation must correspond to a declared field, and a mismatch is a startup failure. Others bind loosely and let default resolution paper over the gaps. Knowing which behaviour your server has is a real, practical thing to know about your stack — and if it binds loosely, the drift is yours to catch. ## Catching it yourself The reliable defences are the ordinary ones, applied to the binding rather than to either artefact alone: * **Prefer strict wiring if your server offers it.** An unmatched name at startup is a thousand times cheaper than a null in production. * **Execute the schema in tests, do not just parse it.** A test that runs a document against the assembled schema exercises the binding; a test that asserts the SDL parses does not. * **Give every new field a test that selects it.** The silent-null drift only shows up when someone actually asks for the field. * **Review the SDL and code halves as one change.** A pull request that touches only the SDL is a claim that the implementation already exists — treat it as a question rather than a formality. ## The comparison worth making Code-first has none of these four drifts, because a field and its implementation are the same declaration: an unimplemented field will not compile. That is genuinely the strongest argument for code-first, and a good answer names it. The price is the subject of its own problems — the contract is derived, so it can move without anyone deciding to move it.

  • Why is a missing implementation for a nullable field harder to notice than for a Non-Null one?
    A nullable field with no implementation resolves to null through default resolution, which is a legal, successful response — no error is raised and nothing is logged. The same gap under a Non-Null field is a field error on the first request that selects it, so it announces itself immediately. Silence is the expensive case.
  • Does the GraphQL specification require a server to reject a schema whose field has no resolver?
    No. The specification validates the type system — interface implementation, argument types, input cycles — but it has no concept of a resolver, so it cannot require a binding to exist. Whether an unmatched field is a startup failure or a silent null is entirely a server-library behaviour, and it varies between libraries.
  • What kind of test actually catches this drift?
    One that executes a document against the assembled schema and asserts on the response, rather than one that only parses the SDL or unit-tests the resolver function in isolation. The drift lives in the binding, so only a test that crosses the binding can see it — which in practice means at least one executed selection per new field.

saying these in an interview costs you the question

  • Believing the compiler checks SDL against resolver code
  • Assuming every server refuses to start on an unmatched field
  • Saying the specification requires a resolver per field
  • Thinking an unimplemented field is hidden from clients
  • Testing that the SDL parses and calling the binding covered

context