skip to content

Why does a federation router add fields to a subgraph fetch that nobody selected?

level: middleimportance: nice to knowfreq 26%

answer

  1. The subgraph gets a rewritten document
  2. The next step must be addressable
  3. Type name plus whatever identifies the object
  4. Removed again before the response is sent
  5. Usage counters on a subgraph over-count

basics

~20 s

Because the next step has to be addressable. Into each fetch the router injects __typename and the key fields of any object whose remaining fields another subgraph owns, then strips those values back out before serialising the response.

solid answer

~50 s

A subgraph never receives the document that was sent to the router. The planner rewrites it, and part of that rewriting is **injection**: it adds `__typename` and the declared key fields for every object that a later fetch will have to address, whether or not the caller selected them. Without them the router could not build the representations the next entity fetch carries, so injection is not an optimisation — it is what makes a multi-step plan possible. The injected values are internal: the router uses them to route and merge, then removes them from the response unless the caller happened to select them too. Two practical consequences follow. A subgraph's logs and field-usage counters see key fields on requests no caller asked them for, so those counters are not a measure of demand. And an expensive-to-compute key field is now computed on every plan that crosses that boundary, not only when it is requested.

code

graphql · 9 lines
graphql
query BoardHome {
  featuredJobs(limit: 12) {
    title
    employer {
      name
      logoUrl
    }
  }
}

go deeper

for a junior

Recall that a subgraph does not see the original document, and that the router adds an identifier so it can ask another service for the rest. Not knowing the detail is not a mark against you.

for a middle

Explain the two injected selections and what each one is for, and be able to say why the caller never sees them. This is the level where the mechanism should feel obvious rather than surprising.

for a senior

Show the operational consequence: subgraph-side field-usage numbers are inflated by plumbing, and a costly key is now paid on every boundary crossing. Say where you would read genuine caller demand instead.

for a principal

Own key design as a cross-team decision with a runtime price. Set the expectation that keys stay cheap and stable, and that telemetry used for deprecation decisions distinguishes router traffic from caller demand.

## The document a subgraph receives is not the one that was sent It is easy to picture a router as a splitter that hands each service the part of the incoming document it owns. The reality is closer to a compiler. Each fetch in a plan is a **newly written operation**: fields the target subgraph does not own are removed, fields it does own are kept, variables are re-declared to match what this fetch actually uses, and — the part people are surprised by — extra selections are added that nobody asked for. ## What gets added, and why Two things, in the common case. `__typename` is added wherever the next step needs to know the concrete type of an object. Federation's entity entry point is typed by `__typename` inside each representation; without it the receiving subgraph cannot tell which type's reference resolution to run. This is also why `__typename` shows up on branches with no interface or union anywhere near them. **Key fields** are added for every object whose remaining fields live elsewhere. If a job posting's employer is answered by the Employers subgraph, then whatever the Employers subgraph declared as that type's key — an id, or a composite selection — has to come back from the Jobs subgraph, because those values are the only handle the router has on that employer. The caller asked for `employer { name logoUrl }`; the fetch that leaves for Jobs asks for `employer { __typename id }`, which shares not one field with the original selection. The rule of thumb worth stating in an interview: **the router injects exactly what the next fetch needs to be addressed by, and nothing more.** If a boundary is never crossed for a given object in a given document, nothing is injected for it. ## And then it disappears Injected values are internal plumbing. The router keeps them long enough to build representations and to merge each returned result back onto the right object, then omits them when serialising. A caller that asked for `employer { name logoUrl }` gets exactly two keys under `employer`. A caller that also selected `id` gets it, of course — in that case the injection was free, because the value was going to be fetched anyway. ## Consequences worth naming **Subgraph telemetry over-counts.** A field-usage dashboard on the Employers subgraph will show `id` on close to every request the router makes into it, and will show it even for documents where no caller mentioned the field. Read at face value, that says `id` is your most demanded field. It is not; it is your most *injected* field. Any decision that leans on subgraph-side usage counts — deprecating a field, sizing a cache — has to separate caller demand from router plumbing, and the router's own field-usage view is the one that reflects demand. **Key cost becomes plan cost.** If a type's key is a plain surrogate id already sitting in the row, injection costs nothing. If it is a value the service computes — a normalised slug, a derived composite — then every single crossing of that boundary pays for it, on every object in the list, whether or not anyone wanted it. That is one concrete reason a cheap, stable key is a design goal rather than a stylistic preference. **Debugging gets easier once you expect it.** An engineer who owns one subgraph and reads its access logs will otherwise spend an afternoon asking who is requesting `__typename` and `id` on everything at a 1,200-request-per-minute peak. Nobody is. The router is. ## What is not injected It is as useful to know what the planner leaves alone. It does not add fields a later step merely *might* want, it does not add the rest of a type for convenience, and it does not add keys for objects whose every selected field is answered by the subgraph already being asked. A type can be an entity, with a declared key, and still be fetched with no injected key at all in a document that never crosses a boundary for it. Injection is driven entirely by the plan the router built for this document, not by the schema in the abstract. ## Specification status The entity entry point and the shape of a representation are part of the Apollo Federation subgraph contract, so the *need* for `__typename` and key fields is not optional. How and exactly when a given router injects them, whether it merges sibling selections into one fetch, whether it preserves the caller's aliases or renames selections internally, and how it handles fragments while rewriting — all of that is planner implementation behaviour and differs between routers. Describe the mechanism confidently; do not attribute a particular rewriting style to "the spec".

  • Do the injected key fields appear in the response the caller receives?
    No, unless the caller selected them independently. The router treats them as internal routing data: it uses them to build representations and to merge each subgraph result onto the right object, then drops them during serialisation. The response contains the caller's own selection set, with its aliases, and nothing added.
  • A subgraph's field-usage dashboard shows one field on nearly every request. What should you check first?
    Whether it is a key field the router injects. Any type whose fields are completed by another subgraph will have its key requested on essentially every crossing, independently of caller demand. Compare against the router's operation-level usage data, which reflects what callers actually selected, before drawing a deprecation or caching conclusion from the subgraph's numbers.
  • Why does __typename show up on a fetch for a plain object type with no interfaces involved?
    Because each representation the next fetch carries is identified by its type name, and the receiving subgraph dispatches reference resolution on it. The planner therefore injects `__typename` alongside the key wherever a boundary will be crossed, regardless of whether abstract types are anywhere in the document.

saying these in an interview costs you the question

  • Says a subgraph receives exactly the caller's selection set
  • Thinks injected key fields end up in the caller's response
  • Believes __typename is only added for unions or interfaces
  • Treats subgraph field-usage counts as caller demand
  • Assumes an expensive key field is computed only when requested

context