skip to content

Why does federation composition read a subgraph schema from _service { sdl } instead of introspection?

level: middleimportance: should knowfreq 38%

answer

  1. Composition needs directives, not just types
  2. Introspection shows definitions, not applications
  3. Deprecation was the one special case
  4. The schema travels as a plain string
  5. It survives introspection being disabled

basics

~20 s

Standard GraphQL introspection exposes directive definitions but never where directives are applied, so the federation directives that describe entities would be invisible. The _service field returns the subgraph's schema as text with those applications intact, and works where introspection is disabled.

solid answer

~50 s

Composition needs to know which types are entities, which fields are borrowed and which are shared — all of that is expressed as directive *applications* on the subgraph schema. Introspection cannot show them: `__schema` lists directive definitions with their arguments and locations, and `__Type`/`__Field` expose names, types and descriptions, but there is no place in the introspection result where an applied `@key(fields: "nctId")` appears. SDL text has no such gap, so the federation subgraph specification puts the schema on the graph itself as `_service { sdl }`, a `String!` containing the subgraph's own schema with its federation directives written out. Two practical benefits come free: it works when introspection is switched off in production, and it is a string a schema registry can store, diff and publish rather than a document that has to be reassembled.

code

graphql · 5 lines
graphql
query {
  _service {
    sdl
  }
}

go deeper

for a junior

Know that a subgraph publishes its schema as a string through a normal query field, and that this string is the SDL text rather than an introspection result.

for a middle

Be able to say precisely what introspection omits — applied directives — and why that omission is fatal for composition, since keys and ownership are expressed only as applications.

for a senior

Discuss the operational side: introspection disabled in production, a registry that stores and diffs the string, and the drift risk when SDL is published from CI instead of read from the running process.

for a principal

Own the distinction between the gates. Composition proves subgraphs fit each other; only checks against recorded client operations prove a change is safe to ship, and the two need separate owners and separate signals.

## What composition actually needs to read Composition takes several subgraph schemas and produces one supergraph. To do that it must know things that live entirely in directive applications: ```graphql type Trial @key(fields: "nctId") { nctId: ID! enrollmentTarget: Int site: Site @provides(fields: "siteCode") } ``` Which types are entities, what their keys are, which fields are borrowed from elsewhere, which may be resolved in more than one place — every one of those facts is `@key`, `@external`, `@shareable`, `@requires`, `@provides` *applied to* a type or a field. ## Why introspection cannot supply them GraphQL's introspection system describes the type system, and it is thorough about definitions: `__schema { types { name kind description fields { name description args { name type } type { kind name ofType } } } }` and `__schema { directives { name args { name } locations isRepeatable } }`. Read that list again for what is missing. You can learn that a directive named `key` exists, that it takes a `fields` argument, and that it is valid on `OBJECT` and `INTERFACE`. You cannot learn that it was *applied* to `Trial` with `fields: "nctId"`, because no introspection type carries applied directives. `__Field` has `name`, `description`, `args`, `type`, `isDeprecated` and `deprecationReason` — deprecation being the one directive whose application was special-cased into introspection, precisely because there was nowhere else to put it. So a composer that introspected a subgraph would see a correctly typed schema with every federation fact erased. It could not tell an entity from a plain object. ## What `_service` returns The federation subgraph specification puts the schema on the graph as data: ```graphql type _Service { sdl: String! } type Query { _service: _Service! } ``` The `sdl` string is the subgraph's own schema in SDL, with its federation directive applications written out as the author wrote them. It should not include the machinery federation adds to every subgraph — `_entities`, `_service`, `_Any`, `_Entity` — because those are identical everywhere and are not part of what the team is publishing. Text has no expressiveness gap: anything you can write in a schema file survives the round trip, including descriptions, extensions, custom directive applications a linter or a policy layer reads, and, in Federation 2, the `@link` that declares which federation edition this subgraph speaks. ## The practical wins **It survives production hardening.** Introspection is routinely disabled on deployed endpoints. `_service` is an ordinary field, so it keeps working under exactly the auth and network path a subgraph already has — one more reason subgraph endpoints are meant to be private rather than public. **It is a value, not a traversal.** A schema registry can store the string, hash it, diff two versions and hand it to a composer offline. Rebuilding equivalent text from an introspection result is lossy in exactly the way described above. **It is self-reported by the running process.** Fetching `{ _service { sdl } }` from a deployed instance tells you what that instance believes it serves. Many teams publish SDL from CI instead — faster, and it works before the service is up — but then the composed graph reflects a file in a repository. If the file and the deployed process ever disagree, the router plans against a schema nobody is serving. ## What this check does *not* do Composition reads subgraph schemas and answers a subgraph-shaped question: do these fit together? It says nothing about clients. Delete `legacyPhaseCode` from a 37-field `Trial` in the registry subgraph and, if no other subgraph mentions it, composition succeeds and publishes a supergraph without the field. A client still pinned to the old document now fails validation at the router with an unknown-field error, and it fails at request time, in production, for that client only. Catching that needs a different input entirely — recorded client operations checked against the candidate schema — which is why schema publication and operation checks are two separate gates, and why the composed schema being green is not the same as the change being safe. ## The interview-sized answer Directive applications are not introspectable, and they are the entire vocabulary of federation, so the schema is fetched as text through a normal field instead. The good follow-up is what the text may contain, and the better one is what composing successfully still fails to prove.

  • Does standard introspection show anything at all about a federation directive such as @key?
    It shows the definition if the subgraph defines it: the name `key`, its `fields` argument, its valid locations, and whether it is repeatable, all under `__schema { directives }`. What it never shows is an application — that `Trial` carries `@key(fields: "nctId")`. Deprecation is the lone exception, surfaced as `isDeprecated` and `deprecationReason` on fields and enum values.
  • Should the sdl string contain the _entities and _service definitions themselves?
    No. The specification asks for the subgraph's own schema, not the additions federation makes to every subgraph, so `_entities`, `_service`, `_Any` and `_Entity` are excluded. Anything the author wrote stays: descriptions, custom directive applications, and in Federation 2 the `@link` declaring the federation edition the schema uses.
  • Teams publish SDL from CI rather than querying a running subgraph. What do they give up?
    The guarantee that the composed graph matches a deployed process. A file can be published before the service ships, after a rollback, or from a branch whose code never reached production, and the router will then plan against a schema nobody serves. The usual mitigation is to fetch `{ _service { sdl } }` from the running instance after deploy and compare it with what was published.

saying these in an interview costs you the question

  • Thinks introspection exposes applied directives
  • Says composition can introspect any GraphQL endpoint
  • Confuses the subgraph SDL with the composed supergraph
  • Assumes successful composition proves clients still work
  • Believes _service returns a JSON introspection document

context