How do you place nullability in a GraphQL schema so one failing field cannot blank the whole response?
answer
- One symbol decides how much you lose
- Walk upward from the risky field
- First nullable link is the boundary
- Firebreak at the smallest usable unit
- Non-Null what the parent already has
basics
~20 sLeave a nullable field on the path between every risky field and the root. A value that cannot be produced for a Non-Null position is absorbed by the nearest nullable ancestor, so that ancestor is the boundary of the damage.
solid answer
~50 sRead every `!` as a decision about loss, not about convenience. To find a field's blast radius, walk from that field up through its parents to the root and stop at the first link that is nullable; everything under that link is what disappears when the field cannot be produced, and if no link is nullable the whole `data` goes null. So place the firebreak deliberately: pick the smallest part of the response a consumer can still render without the risky field, and make *that* link nullable. In a payroll and benefits graph that usually means `payslip.benefitElections` is nullable while `payslip.id` and `payslip.netPay` stay Non-Null, because those come from the row already loaded. Nothing in the GraphQL specification says where `!` belongs — this is schema-design judgement, and the interviewer is testing whether you make it consciously.
code
graphql · 15 linestype Query {
payRun(id: ID!): PayRun!
}
type PayRun {
id: ID!
periodEnd: Date!
payslips: [Payslip!]!
}
type Payslip {
id: ID!
netPay: Money!
benefitElections: [BenefitElection!]!
}go deeper
Be ready to say what ! promises and that the promise has a cost: when the server cannot keep it, more than that one field goes missing. Knowing that much, and that the schema author chooses it, is enough at this level.
Explain the walk: from the risky field up to the root, the first nullable link is the boundary, and no nullable link means the whole response. Then justify a specific placement in a schema you are shown, and name the fields that are safe to keep Non-Null.
Show that you treat ! as an availability decision made at design time. Talk about which fields cross a service boundary, what the smallest independently renderable unit is, and how you would review a schema for chains that reach the root unbroken.
Own the tradeoff between resilience and consumer ergonomics. Blanket nullability makes null ambiguous and pushes branching into every client; blanket Non-Null makes one dependency able to blank a page. Be ready to state the rule your organization would enforce and how you would check it.
## What `!` buys, and what it charges In GraphQL, `!` marks a position as **Non-Null**: the server promises the client that this position will never hold null. That promise is worth something. A consumer needs no branch, and a typed client generator emits the member as required rather than optional. But the promise has to hold on the worst day too, and when it cannot, GraphQL does not quietly put a null in the slot. The unfulfillable Non-Null position is resolved by nulling the **nearest nullable ancestor**, and everything that had already resolved underneath that ancestor — sibling fields, sibling list entries — is discarded with it. So a `!` is not a free ergonomic win. It is a statement of the form *"if this value is missing, throw away everything down to here."* The region thrown away is that field's **blast radius**, and the schema author is the only person who chooses it. ## Reading a schema for blast radius The procedure is mechanical. Start at a field you consider risky — typically one backed by a separate service, a queue, a cache that can miss, or a computation with a timeout. Walk upward through its parent fields to the root operation field. Stop at the first link whose type is **not** Non-Null. Everything beneath that link is the blast radius. If you reach the root without finding a nullable link, the blast radius is the entire response: the client gets `data: null` and none of the work that succeeded. Consider a payroll and benefits graph in which every link is Non-Null: ```graphql type Query { payRun(id: ID!): PayRun! } type PayRun { id: ID! periodEnd: Date! payslips: [Payslip!]! } type Payslip { id: ID! netPay: Money! benefitElections: [BenefitElection!]! } ``` `benefitElections` is served by the benefits provider; `id` and `netPay` come from the payslip row the parent already loaded. The chain from `benefitElections` to the root has no nullable link, so when the provider is unreachable for one employee out of 4,300, the response for the entire pay run is `data: null`. Thousands of correctly computed payslips are dropped because one optional-looking side panel could not be filled. ## Placing the firebreak The design question is not "should this be nullable?" in isolation. It is: **what is the smallest unit of the response a consumer can still use without this field?** Make the link at that boundary nullable, and leave everything else alone. In the example the answer is a single payslip's benefits panel, so `benefitElections` itself becomes nullable. One payslip now arrives with a hole where the elections were; every other field of every payslip survives. Making `payslips` nullable instead would be a firebreak too, but a badly placed one — it costs the whole pay run to save the same panel. Making the root `payRun` nullable is no firebreak at all: it is where the damage was already going. Two heuristics decide most fields: * **Non-Null is cheap when the field comes from the same load as its parent.** If the parent object exists, the field exists: an identifier, a column of the row in hand, a value computed from data already present. There is no separate way for it to fail. * **Non-Null is expensive across a boundary.** Another service, a network hop, a cache miss, a nightly job that may not have run. Each of these fields is a fuse, and the `!` on it and on every ancestor sets how much burns. List fields carry two independent decisions — the list itself and each element — and choosing them per element is its own exercise; the point here is only that both wrappers are part of the same chain. ## The cost of over-correcting "Make everything nullable" is not the answer either. Every nullable field is a branch in every consumer, and a null becomes ambiguous: for a nullable `benefitElections`, null may mean *this employee has no elections on file* or *we could not reach the provider*, and the two are distinguishable only by whether the response's errors list carries an entry whose path points at that field. Nullability is a budget, spent where it buys resilience and saved where it buys clarity. ## Say that the specification is silent The GraphQL specification defines the Non-Null wrapping type, defines what happens when a null lands in a Non-Null position, and defines the error entry that accompanies it. It says **nothing** about where a schema author should put `!`. The house rules you will hear — *non-null only what you can always produce*, *nullable across a service boundary*, *non-null identifiers* — are widespread convention, not specified rules. Saying that out loud in an interview is worth as much as the placement heuristic itself, because it shows you know which part of GraphQL is a standard and which part is taste.
- If every link on the path is nullable, is the blast radius zero?The radius is as small as it can be — only the failing field is empty — but the cost has moved to the consumer. Every field is now a branch, and null becomes ambiguous: it may mean the value legitimately does not exist or that producing it failed. The only way to tell them apart is to read the response's errors list and match an entry's path to the field. Blanket nullability trades one problem for another.
- Which fields are genuinely safe to mark Non-Null?Fields that cannot fail independently of their parent: an identifier, a column of the row already fetched, a value computed from data in hand. If the parent object was produced, the field was produced with it, so the promise costs nothing. A field reached by a second call — another service, a cache that can miss, a job that may not have run — is a promise you cannot keep on a bad day, and the `!` there is what turns a bad day into a blank response.
- Does the GraphQL specification give any guidance on where `!` belongs?None. The specification defines the Non-Null wrapping type, defines what happens when a Non-Null position cannot be filled, and defines the error entry that accompanies it. Placement is entirely a schema-design decision. The familiar rules of thumb — non-null identifiers, nullable across a service boundary, nullability as a budget — are convention that grew out of practice, and it is worth naming them as convention rather than attributing them to the specification.
Non-Null markers are the bulkheads in a ship's hull. Weld them all shut end to end and one puncture floods everything; leave a bulkhead at the right frame and you lose one compartment.
saying these in an interview costs you the question
- Marks every field Non-Null to simplify client types
- Thinks Non-Null guarantees the data is always available
- Believes a failure only affects the field that failed
- Puts ! on a field fetched from a separate flaky service
- Cannot say where the damage from a failure stops
- Claims the spec prescribes where ! belongs