skip to content

How would you set a nullability policy for a large GraphQL schema, given Non-Null is a one-way door?

level: principalimportance: should knowfreq 36%

answer

  1. One of the two directions is irreversible
  2. Prefer the reversible option, buy information
  3. Guaranteed by identity, not by today's rows
  4. Both extremes have a real cost
  5. State the invariant without the word currently

basics

~20 s

Treat each Non-Null as a promise you can never withdraw without a client migration. Default to nullable, spend Non-Null only on values guaranteed by an object's identity, and make adding one a reviewed decision rather than a stylistic default.

solid answer

~50 s

There is no correct global answer, only a posture and a process. My posture is nullable by default with Non-Null spent deliberately, because the specification's default is the reversible one: a nullable field can be tightened later when the invariant is proven, whereas a Non-Null field can only be loosened by breaking every shipped client. The rule I ask teams to apply is whether the value is guaranteed by the object's identity rather than by today's rows — an `id` qualifies, a `certificate` does not. The process matters as much as the rule: adding a `!` to a published output field is a schema-review decision, and on a schema many teams write into I would rather absorb the ergonomic cost of null checks than accumulate promises the org cannot keep. The counterweight is real, though — a schema that promises nothing pushes defensive branching into every client.

code

graphql · 7 lines
graphql
type Enrollment {
  id: ID!            # guaranteed by identity
  enrolledAt: DateTime!  # guaranteed by identity
  section: Section!  # cannot exist without one
  finalGrade: String # true only once graded
  certificate: Certificate # absent for 1,463 imported rows
}

go deeper

for a junior

You will not be asked to set the policy, but know which way it points: fields start nullable, and ! is something a team adds on purpose. If you find yourself adding one for convenience, ask someone first.

for a middle

Be able to justify a specific field's nullability in review. Practise stating the invariant in one sentence without the word "currently" — if you cannot, that is your answer, and it is a useful thing to say out loud.

for a senior

Show you can weigh both failure modes rather than only the over-promising one, and give a testable rule such as the identity test. Interviewers listen for whether you would enforce the policy in CI or merely publish it.

for a principal

Own the irreversibility argument and concede its cost: a maximally nullable schema exports uncertainty to every client. Be ready to say how the posture changes when the organisation cannot recall its own clients, and how you would revisit it.

## Why this is a policy question at all Nullability looks like a per-field style choice and behaves like an architectural commitment. Each `!` on an output field is a promise made to every client that will ever exist, and the only way to withdraw it is a coordinated migration of everything already deployed. A schema accumulates these promises silently, one pull request at a time, from authors optimising for the ergonomics of the client they are looking at today. That is the ratchet a policy exists to govern. Consider a course-enrolment graph with 214 registered client operations, whose deepest mobile document runs 19 levels from the root down through a learner's enrolments, sections, instructors and issued certificates. Every `!` along that 19-level path is an independent promise, and the promises compound: the deeper a Non-Null sits, the more of the graph had to be reachable and producible for it to hold, and the more future data sources have to honour it. Nobody decided that; nineteen separate authors each made a locally reasonable choice. ## The two failure modes, and neither is free **Over-promising.** The schema is dense with `!` because it made client code tidy. Then a new tenant, an acquisition's back-catalogue, or a backfill of 1,463 legacy enrolments arrives with the value genuinely absent. Now every option is bad: loosen the field and break 37 shipped builds, invent a sentinel value that lies, or refuse the data. **Under-promising.** Everything is nullable, including things that cannot possibly be absent. Clients drown in null checks, generated types are nullable all the way down, and the schema stops communicating anything — a field being nullable no longer carries information, because they all are. Teams then re-encode the real invariants in comments, which no tool reads. A policy has to name where between these it sits and why, and be honest that it is a tradeoff rather than a correctness rule. ## The posture I would set **Default nullable; spend Non-Null.** The asymmetry decides it. A nullable field can be tightened later once the invariant has survived contact with real data, and the tightening costs clients nothing. The reverse costs a migration. Given an irreversible and a reversible option with similar upside, take the reversible one and buy information. **The identity test.** A `!` is justified when the value is guaranteed by what the object *is*, not by what the current rows happen to contain. An `id` is guaranteed by identity. A `createdAt` usually is. A relationship the object cannot exist without usually is. A certificate, a final grade, anything sourced from a system that can be unavailable, or anything true only "currently" is not. **Non-Null on published output fields is a review decision, not a style choice.** The edit is cheap for clients and permanent for the org, which is exactly the shape of change that slips through review unremarked. Naming it explicitly is most of the control. **Distinguish the positions.** The policy is about output fields. Arguments and input fields run the opposite way — loosening is the safe direction there — so a single "be strict" or "be loose" slogan applied schema-wide is wrong in one of the two positions by construction. ## Making the policy operational A posture nobody can check is a preference. Three things make it real: - **Diff the proposed schema against the published one in CI** and treat an output field gaining or losing a Non-Null as a change requiring a named approver, not an automatic pass. - **Require the invariant in prose.** If the author cannot state, in one sentence with no "currently", why the value can never be absent, the field stays nullable. This single question removes most speculative `!`s. - **Let evidence unlock the tightening.** A field that has been nullable for two quarters and has never once been observed null is a far better candidate for `!` than a new field someone is sure about. ## What I would concede I would not pretend the ergonomic cost is imaginary. The strongest counter-argument is that a maximally nullable schema exports its uncertainty to every client, multiplied by the number of clients, and that a schema promising nothing has stopped being a contract. So the policy is not "never promise" — it is "promise where the domain guarantees it, at a deliberate moment, with an owner". And I would revisit the posture as the organisation changes: a single team shipping one client it controls can afford promises that a schema serving mobile builds it cannot recall cannot. The runtime consequences of a `!` are a separate axis of the same decision, and a mature policy names both. ## The interview shape of a good answer Name the irreversibility, name both failure modes rather than only over-promising, give a testable rule instead of a vibe, say how it is enforced, and concede the cost of your own posture. A candidate who says "make everything non-null, clients hate null checks" has answered for the client they can see and not for the schema they will own.

  • How do you decide a nullable field has earned its Non-Null after the fact?
    With evidence rather than confidence. A field that has been nullable across several quarters, many tenants and at least one backfill without ever being observed null is a real candidate; a field someone is merely sure about is not. Pair that with a stated invariant the author can express without the word "currently", and treat the edit as a reviewed change.
  • What is the strongest argument against your default-nullable posture?
    That it exports uncertainty to every client, multiplied by the number of clients: generated types become nullable all the way down, and a schema where everything is nullable no longer communicates anything, because nullability stops carrying information. The honest answer is that both extremes cost something and the posture is a bet on which cost is recoverable.
  • Should the same policy apply to arguments and input fields?
    No — inverted, because the safe direction reverses in the input position. Loosening an argument is the cheap direction for clients and tightening is the breaking one, so a schema-wide "be strict" or "be permissive" slogan is guaranteed to be wrong in one of the two positions. Policies have to name the position they govern.
  • How would you enforce this on a schema many teams write into?
    Diff each proposed schema against the published one in CI and make an output field gaining or losing a Non-Null require a named approver rather than passing silently. Add a review question that asks for the invariant in one sentence. Enforcement matters more than the rule, because these edits look like formatting in a large diff.

Non-Null is a warranty printed on a product already in customers' hands: easy to add to the next print run, impossible to withdraw from the copies out there. You issue it where the guarantee is structural, not where the last batch happened to be flawless.

saying these in an interview costs you the question

  • Says make everything Non-Null so clients skip null checks
  • Treats nullability as a formatting preference
  • Names only over-promising as a failure mode
  • Applies one strictness slogan to outputs and arguments alike
  • Offers a posture with no way to enforce it
  • Claims the specification prescribes a nullability policy

context