skip to content

Why must fragment spreads in a GraphQL document never form a cycle?

level: middleimportance: nice to knowfreq 24%

answer

  1. Expansion, not invocation
  2. Finite text, infinite selection set
  3. Draw the fragments as a directed graph
  4. Cyclic schema fine, cyclic spreads not
  5. Fails identically against an empty database

basics

~20 s

Spreading a fragment inlines its selections rather than calling it, so two fragments that spread each other denote a selection set with no end. The specification makes cycles a validation error, so such a document fails before execution.

solid answer

~50 s

A fragment spread is textual expansion: the fragment's selection set is included at the point of the spread. If `EmployeeCore` spreads `ManagerSummary` and `ManagerSummary` spreads `EmployeeCore`, expanding either one never terminates — the document is finite but the selection set it denotes is infinite. The specification therefore carries a validation rule that fragment spreads must not form a cycle, direct or indirect, so such a document fails before execution with errors and no data key. The contrast worth stating is that recursive *types* are entirely legal: a schema may declare `Employee.manager: Employee`, and a document that wants three levels of management chain simply writes three nested selection sets. The document's own text bounds the depth; a fragment cycle would remove that bound, which is exactly why the rule exists at the document level and not in the schema.

code

graphql · 19 lines
graphql
query OrgChart {
  employee(id: "E-4182") {
    ...EmployeeCore
  }
}

fragment EmployeeCore on Employee {
  fullName
  manager {
    ...ManagerSummary
  }
}

fragment ManagerSummary on Employee {
  fullName
  directReports {
    ...EmployeeCore
  }
}

go deeper

for a junior

Recall that a spread pastes a fragment's selections in place, and that a document where two fragments spread each other is rejected outright. You are not expected to recite the rule's name, only to see why it must exist.

for a middle

Explain the expansion model and why it terminates only for an acyclic spread graph, and be ready to draw the contrast with a schema whose types reference each other, which is perfectly ordinary.

for a senior

Show how you would recognise this in the field: a document that fails totally and identically for every caller, even against an empty dataset, is a static problem, and the fix belongs in how the client composes fragments rather than in any resolver.

for a principal

Speak to the tradeoff the language chose — a strictly acyclic document keeps every request statically analysable, at the cost of forcing clients to write recursive shapes out to a fixed depth or to model traversal explicitly in the schema.

## A spread is expansion, not a call The mental model that makes the cycle rule obvious is this: spreading a fragment is not invoking a function that returns a value. It is *including that fragment's selection set at the point of the spread*. When a server collects the fields for a selection set, it walks the selections, and every time it meets `...SomeFragment` it substitutes that fragment's own selections and keeps walking. There is no call stack, no return, and no notion of "the same fragment, one level down". Now put two fragments in a payroll and benefits graph that reference each other: ```graphql fragment EmployeeCore on Employee { fullName manager { ...ManagerSummary } } fragment ManagerSummary on Employee { fullName directReports { ...EmployeeCore } } ``` Expanding `...EmployeeCore` requires expanding `...ManagerSummary`, which requires expanding `...EmployeeCore`, and so on. The document text is finite and perfectly well formed, but the selection set it *denotes* is infinite. There is no natural place to stop, because the document never says how deep the org chart goes. ## The rule The specification therefore states a validation rule of its own: the fragment spreads in a document must not form a cycle. It covers the direct case — a fragment that spreads itself — and every indirect chain, however long. Read the fragment definitions as a directed graph, with an edge from each fragment to every fragment it spreads anywhere inside itself, including inside nested selections and inline fragments. If that graph has a cycle, the document is invalid and the request fails before execution begins: no resolver runs, and the response carries errors with no data key. Notice what the rule is *not*. It is not a depth limit, not a size cap, and not a runtime safeguard that stops after so many expansions. It is a structural property of the document, decidable from the document alone, and it is checked once in the same static pass that checks field names and required arguments. ## Recursive types are legal; recursive fragments are not This is the distinction that separates a candidate who has thought about it from one who has memorised a rule. A GraphQL schema may be thoroughly cyclic: ```graphql type Employee { fullName: String! manager: Employee directReports: [Employee!]! } ``` That is not merely allowed, it is ordinary — most real graphs are cyclic, which is why they are called graphs. The type-level cycle is harmless because a *document* must write out every level it wants explicitly. A client that needs three levels of management chain writes three nested selection sets, and the document's own text bounds the depth. Fragments break that bound: a cycle would let a finite amount of text denote an unbounded selection, so the rule closes it at the document level while leaving the schema free. ## The neighbouring hygiene rules The cycle rule sits with two siblings that come from the same instinct — a document should say exactly what it means and nothing else. A spread must name a fragment that is actually defined in the same document. There is no import mechanism in the language; whatever a client's tooling does to assemble fragments from separate source files, what arrives on the wire must be one self-contained document, and a spread whose target is absent fails validation. And every fragment defined in the document must be spread at least once. A definition nobody uses is dead weight the server would parse and hold for no reason, and far more often it is the fingerprint of a bug: a client assembled a document from several sources and dropped the selection that was supposed to use the fragment. Rejecting it turns a silent mistake into an immediate, located error. ## Diagnosing one in practice Cycles rarely get written by hand. They appear when fragments are composed by tooling or by convention — one fragment per screen area, each pulling in the next — and two of them end up mutually referential through a relationship such as `manager` and `directReports`. The symptom is unmistakable once you know the rule: the failure is total, immediate and identical on every request carrying that document, with an errors list and no data key, and it reproduces with no data in the system at all. That last property is the tell. Anything that fails identically against an empty database is a static problem, and the fix is in the document rather than in a resolver: break the loop by writing the levels you actually need explicitly, and keep each fragment's spreads acyclic.

  • How would a server detect an indirect cycle across four fragments?
    Build a directed graph whose nodes are the fragment definitions in the document and whose edges run from each fragment to every fragment it spreads, at any nesting depth inside it, including inside inline fragments. Then look for any cycle in that graph. The check needs only the document, costs time proportional to the number of spreads, and runs in the same static pass as the other rules.
  • If cycles are forbidden, why can a document still nest very deeply?
    Because nesting depth comes from the selection sets a client writes out by hand, and the schema being cyclic lets it write as many levels as it likes. The cycle rule only stops a finite document from denoting an infinite selection; it places no cap on explicitly written depth. Bounding that is a separate, unspecified concern handled by server-side limits.
  • Why does validation also reject a fragment that is defined but never spread?
    Because a document should carry exactly what it means. An unused definition is work the server does for nothing, and in practice it is usually evidence of a bug — a client composed the document from several sources and lost the selection that was meant to spread it. Failing early turns a silent assembly mistake into a located error.

A fragment spread behaves like a text macro rather than a function call — and a macro that expands to itself has no base case to stop at.

saying these in an interview costs you the question

  • Thinks a fragment spread is a function call
  • Says the server expands the cycle to a default depth
  • Claims recursive types in the schema are illegal too
  • Believes the cycle surfaces as a runtime stack overflow
  • Confuses the cycle rule with a query depth limit
  • Assumes an unused fragment is quietly ignored

context