skip to content

Federation & Composition

Many service schemas composed into one client-facing graph under Apollo Federation — a specification of its own, not part of GraphQL's. Interviewers raise it wherever GraphQL meets microservices.

part ofGraphQLoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

In Apollo Federation, why does composition fail when two subgraphs declare the same field with different types?

level: juniorimportance: must knowfreq 58%

answer

  1. One merged schema, one type per field
  2. Not concatenation — a merge
  3. Wrapper differences are not name differences
  4. Outputs widen, inputs narrow
  5. All or nothing, no partial supergraph

basics

~20 s

Composition merges the subgraph schemas into one supergraph, so every field ends up with exactly one type. Two different named types cannot both be published, so composition reports an error and produces no supergraph at all.

solid answer

~50 s

Composition is a merge, not a concatenation. Where several subgraphs declare the same type, their declarations are folded into one, and the client-facing schema publishes one type per field. If `Sensor.lastCalibratedAt` is `String!` in one subgraph and `DateTime!` in another, no single published type is honest about both, so composition rejects the pair and emits no supergraph at all — not a supergraph with that field left out. Differences that are only about nullability are a separate case: they are merged by a variance rule, because the merged type has to describe every value a subgraph might return, and has to be accepted by every subgraph that receives an argument. Two different **named** types never merge in either direction. Argument lists drift the same way: an argument required in one subgraph and absent in another is a hard error.

code

graphql · 13 lines
graphql
# Devices subgraph
type Sensor @key(fields: "id") {
  id: ID!
  lastCalibratedAt: String!
  readings(since: DateTime, limit: Int = 50): [Reading!]!
}

# Agronomy subgraph
type Sensor @key(fields: "id") {
  id: ID!
  lastCalibratedAt: DateTime!
  readings(since: DateTime, limit: Int = 50, unit: ReadingUnit!): [Reading!]!
}

go deeper

for a junior

Remember that all the subgraph schemas are merged into one before clients see anything, so a field can only have one type. If two services declare it differently, that is a build failure to raise, not something the router sorts out.

for a middle

Be ready to separate the cases: a different named type is fatal, while nullability and list wrappers are merged by direction — outputs widen so clients can receive null, arguments narrow so every subgraph accepts what arrives.

for a senior

Show where these come from in a real graph: copied value types drifting across teams, and argument lists gaining a required argument in one service. Talk about running composition on every subgraph change and about additive migrations rather than simultaneous flips.

for a principal

Own the structural fix. Decide whether shared shapes are copied by convention or generated from one source, who arbitrates when two teams want different types, and how a graph of dozens of subgraphs keeps a conflict from blocking every other team's release.

## Composition is a merge, and a merge needs one answer Apollo Federation splits one client-facing graph across independently deployed services. Each service publishes a **subgraph schema**; a composition step reads all of them together and produces a single **supergraph schema**, which the router uses to plan queries and from which the client-facing API schema is derived. Composition is not a concatenation of files. Wherever several subgraphs mention the same type, it has to merge those declarations into one, and every merged field carries exactly one type in the result. That is the whole reason a type mismatch is fatal. Take a farm-sensor graph, where a Devices subgraph owns the hardware and an Agronomy subgraph owns readings: ```graphql # Devices subgraph type Sensor @key(fields: "id") { id: ID! lastCalibratedAt: String! } # Agronomy subgraph type Sensor @key(fields: "id") { id: ID! lastCalibratedAt: DateTime! } ``` There is no type the API schema could publish for `lastCalibratedAt` that is honest about both. Publishing `String!` withdraws a parsing and serialization contract Agronomy's clients rely on; publishing `DateTime!` promises validation the Devices subgraph never performs. So composition stops, names the field and the subgraphs that disagree, and produces nothing. This is the property people mean when they say composition **fails closed**: the output is a whole supergraph or a list of errors, never a partial supergraph with the offending field quietly dropped. ## Nullability is a different question from the named type Candidates often collapse the two into one rule — "the types must be identical" — and that overshoots. Wrapper differences (`!` and lists) are handled by a variance rule, because there is a defensible direction for each position: - A field the client **reads**. The published type must describe every value any subgraph could return. If one subgraph says `String!` and another `String`, a client can be handed null, so the safe merge is the nullable one. Outputs widen. - An argument the client **supplies**. The published type must be accepted by every subgraph that will receive the value. If one subgraph takes `Int` and another `Int!`, publishing the nullable form would let a client send null into a subgraph that rejects it, so the safe merge is the non-null one. Inputs narrow. Two different named types never merge under either rule. `String` and `DateTime` are unrelated leaf types; there is no widening that covers both and no narrowing either satisfies. The same is true of an object type against a scalar, or two different enums. ## Arguments drift as well as return types The second family of mismatch is the argument list on a shared field: ```graphql # Devices subgraph readings(since: DateTime, limit: Int = 50): [Reading!]! # Agronomy subgraph readings(since: DateTime, limit: Int = 50, unit: ReadingUnit!): [Reading!]! ``` An argument only some subgraphs declare cannot be honoured everywhere, so it does not survive into the client-facing schema — clients simply cannot pass `unit`, and composition usually says so as a hint rather than an error. If that argument is **required** in the subgraph that declares it, there is no such escape: the router would have to invent a value for the subgraphs that never asked for one, so composition fails outright. Default values that differ are the quiet version of the same problem — the published default is one of them, and the other subgraph's callers change behaviour. ## Where these conflicts actually come from In a 62-subgraph supergraph, most type mismatches are not on entities at all. They are on small shared shapes — a `GeoPoint`, a `Measurement`, a `TimeWindow` — that several teams copied into their own SDL and then edited independently. One team makes `Measurement.unit` non-null after backfilling their store; another adds a field; a third changes a timestamp from `String` to a custom scalar. Nothing is wrong in any single repository, and each subgraph is a perfectly valid GraphQL schema on its own. Composition is the first place the divergence is visible, which is why it should run on every subgraph change rather than at release time. ## What is specified here, and what is not None of this is in the GraphQL specification. GraphQL knows nothing about subgraphs, supergraphs or merging; it validates one schema at a time. Merge rules, satisfiability and the fail-closed behaviour come from Apollo Federation's composition specification, and the exact wording and codes of the errors are implementation detail — quote the rule in an interview, not an error code you half-remember. ## Fixing one The repair is a conversation between the two owners, not a flag. In practice: converge on one type additively — add the new field or the new scalar alongside the old one, migrate clients, then delete the old — because a same-day flip in both subgraphs only works if both deploy at once. Or give the field a single owner and let the other subgraph reference the entity by key instead of redeclaring the field. For the copied value types, the durable fix is one generated source of truth for the shared shapes, so there is only ever one definition to drift from.

  • If one subgraph declares a shared field as String! and another as String, does composition fail?
    No — that is a wrapper difference, not a different named type, and it is merged by a variance rule. For a field a client reads, the published type has to cover every value a subgraph might return, so the nullable form is the safe merge. Arguments go the other way: the published type must be one every subgraph accepts, so the non-null form wins there.
  • Why does composition refuse to publish a supergraph that simply omits the conflicting field?
    Because a silently smaller graph is worse than a failed build. Dropping the field would break every client selecting it, with no signal to the team that caused it, and the same rule applied at scale would let a graph erode field by field. Composition is all-or-nothing on purpose: you get a complete supergraph or a list of errors to fix.
  • Is any of this defined by the GraphQL specification?
    No. The GraphQL specification validates a single schema and says nothing about merging several. Subgraphs, supergraphs, merge rules and composition errors come from Apollo Federation's composition specification, which sits on top of GraphQL. Each subgraph schema is independently valid GraphQL — that is exactly why these conflicts only appear when the schemas are read together.

Two branches printing the same catalogue can differ on layout, but the price of an item has to be one number before the catalogue can go to print.

saying these in an interview costs you the question

  • Thinks the router picks a type at query time
  • Says composition emits a supergraph missing the bad field
  • Believes every shared field must match byte for byte
  • Cannot separate a wrapper difference from a type difference
  • Assumes the GraphQL specification defines these merge rules
  • Fixes it by flipping both subgraphs in one release with no migration

context

open as a page

In a federated graph, why does the router batch entity fetches instead of one call per item?

level: juniorimportance: must knowfreq 56%

basics

~20 s

One call per item is an N+1 across services, where each call is a full subgraph request. The router instead collects every item's key into a single _entities call carrying a list of representations, so a 143-item list costs one request.

open as a page

In GraphQL, what does a client see when many services sit behind one endpoint?

level: juniorimportance: must knowfreq 57%

basics

~20 s

One schema and one endpoint. The client sends a single operation and gets one response, and nothing in that response says which service produced which field. The split across services is a server-side arrangement the caller never sees.

open as a page

What is a query plan in a federated GraphQL supergraph, and what is each step?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A query plan is the ordered set of subgraph requests a federation router derives from one client document. Each step is a complete GraphQL operation sent to exactly one subgraph, and the router merges the results into a single response.

open as a page

In GraphQL schema stitching, what does the gateway hold that the services do not?

level: juniorimportance: must knowfreq 53%

basics

~20 s

The gateway holds the merge configuration: renames that resolve name collisions, plus delegation rules saying which field on one service's type is answered by calling which operation on another. The stitched services stay ordinary GraphQL servers, unaware of each other.

open as a page

In a federated subgraph schema, what does @key(fields: "id") on a type declare?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It marks the type as an entity: an object other subgraphs may reference. The fields argument is the selection set that identifies one instance, and the subgraph declaring it promises it can look an instance up from those fields alone.

open as a page

In a federated graph, what does a client receive when one subgraph fetch fails?

level: juniorimportance: must knowfreq 57%

basics

~20 s

One ordinary response carrying both halves: data holding everything the healthy subgraphs returned, and an errors entry whose path names the field the failed fetch should have filled. Partial data is the normal outcome, not an outage.

open as a page

In Apollo Federation, how do the subgraph, supergraph and API schemas differ?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Each service publishes its own subgraph SDL. Composition merges those into one supergraph schema that also records which service owns each field. The API schema is that same graph with all federation machinery stripped out - the only one clients introspect.

open as a page

What does a federated subgraph's _entities field receive in each representation?

level: middleimportance: must knowfreq 62%

basics

~20 s

A representation is a plain JSON object carrying __typename plus the fields of one key the subgraph declared for that entity type, and nothing else is guaranteed. It arrives as an untyped scalar, so the schema validates none of it.

open as a page

In Apollo Federation 2, why can only one subgraph resolve a field unless it is @shareable?

level: middleimportance: must knowfreq 58%

basics

~20 s

Federation 2 assumes one owner per field so the router always knows where to fetch it and the answer never depends on the query plan. @shareable is the explicit opt-out: several subgraphs may resolve the field, and they promise to agree.

open as a page

In a federation query plan, what forces one subgraph fetch to wait for another?

level: middleimportance: must knowfreq 55%

basics

~10 s

A data dependency. A fetch waits only when its input comes from an earlier fetch's result — usually the key values identifying its objects. Fetches whose inputs are already available run concurrently.

open as a page

In a federated GraphQL graph, how does a caller's identity reach each subgraph?

level: middleimportance: must knowfreq 62%

basics

~20 s

A federated router does not forward the client's HTTP request; it issues a fresh request per subgraph fetch. Identity travels only if the router is configured to copy the credential onto each one, and every subgraph must verify it itself.

open as a page

How do composite and nested @key selection sets work in a federated subgraph?

level: middleimportance: must knowfreq 50%

basics

~20 s

The fields argument is a selection set, not a list. Space-separated names make one composite key from several fields; braces reach into a sub-object. All named fields together form one identity, and each must be resolvable in the declaring subgraph.

open as a page

In a federated supergraph, what is a satisfiability error when every subgraph schema is valid on its own?

level: seniorimportance: must knowfreq 46%

basics

~20 s

It reports a field in the merged graph that no query plan can reach: the router would have to enter the subgraph holding it, and no shared key allows that jump. Each schema is valid alone; the graph is not.

open as a page

When does splitting a GraphQL graph across services pay off, and when does one server still win?

level: seniorimportance: must knowfreq 59%

basics

~20 s

Team autonomy is the payoff: each team ships its slice of the schema without a shared release train. It is not a performance win — composition adds hops and failure modes. With few contributors on one cadence, one server still wins.

open as a page

Why can a subgraph's _entities field bypass the authorization it enforces on root fields?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Because the router never calls the subgraph's own root fields for an entity another subgraph referenced. It calls _entities with key representations, so a check written inside a root-field resolver is simply not on that path.

open as a page

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

level: middleimportance: should knowfreq 38%

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.

open as a page

What are the three routes to one client-facing GraphQL graph over many services?

level: middleimportance: should knowfreq 51%

basics

~20 s

A single aggregating server whose resolvers call the backends; a gateway that stitches remote GraphQL schemas using mapping held centrally; and a composed graph, where each service declares its own contribution and a composition step merges them.

open as a page

In a federated graph, why can one subgraph's failure erase data other subgraphs returned?

level: middleimportance: should knowfreq 51%

basics

~20 s

Because the failed field was declared Non-Null. A null cannot sit there, so it climbs to the nearest nullable ancestor, discarding sibling fields the router had already collected from healthy services. Nullability at ownership seams sets the blast radius.

open as a page

What do the join__ directives inside a composed federation supergraph schema record?

level: middleimportance: should knowfreq 41%

basics

~20 s

They record the routing metadata composition adds: a generated join__Graph enum lists each subgraph with its URL, @join__type says which subgraphs declare a type and with what key, and @join__field says which subgraph resolves each field.

open as a page

A federated subgraph deployed hours ago, but its new field is still missing from the graph — why?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Almost always the schema failed to compose. Composition is all-or-nothing, so no supergraph was produced and the router kept serving the last one that composed. The service is running new code while the graph still describes the old schema.

open as a page

Why must a subgraph's _entities resolver return its results in the input order?

level: seniorimportance: should knowfreq 47%

basics

~20 s

The router matches results to representations by position, not by identity. A reordered or shortened list silently attaches one entity's fields to a different entity. Return exactly one element per input, in order, with null where the entity could not be resolved.

open as a page

A subgraph gets one entity call carrying 4,812 representations. How do you bound that fan-out?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Bound the list that produces it: require a paginated argument with an enforced maximum, and score document cost at the router before planning. Then chunk oversized representation lists and let the subgraph reject calls above its own cap.

open as a page

How does @override(from:) move field ownership between subgraphs safely?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The receiving subgraph declares the field with @override(from:) naming the subgraph it is taking over from; composition then routes every request for that field to the new one. Deploy the new implementation first, compose, verify, then delete the old field.

open as a page

When does @provides in a federated subgraph actually save the router a fetch?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Only when the client reaches the entity through the exact field carrying the directive, and only if the resolver really returns the promised values. Reached by any other path, the router still fetches the field from the subgraph that owns it.

open as a page

Two federated query plans issue the same number of subgraph fetches, but one is far slower. Why?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Depth, not count. End-to-end latency follows the longest chain of dependent fetches, because each round waits for the previous one to return. Six fetches in two concurrent rounds cost about two hops; six chained fetches cost six.

open as a page

How would you migrate a stitched GraphQL gateway to a composed graph without a flag day?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Treat the client-facing schema as the invariant and move one type at a time: register the remaining stitched gateway as a single subgraph of the composed graph, then lift types out of it, diffing the schema and replaying real traffic.

open as a page

A federated subgraph trusts an x-user-id header its router sets. Why is that unsafe?

level: seniorimportance: should knowfreq 43%

basics

~20 s

The header is unverified text, so the subgraph is trusting whatever can open a connection to it. It is safe only if nothing but the router can reach the subgraph and the router strips any copy the caller sent.

open as a page

What goes wrong when a federated entity's @key is not actually unique?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Nothing detects it. Composition takes uniqueness on faith, so colliding key values make two objects look like one: lookups return whichever row matched first, and any layer keyed on the identity serves one viewer the other's data.

open as a page

How do you set per-fetch timeouts in a federated router so one slow subgraph cannot stall a request?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Give the whole operation one deadline, derive each fetch's timeout from the budget left rather than a fixed constant, size it from that subgraph's own latency distribution, and propagate the deadline downstream so an abandoned fetch stops working.

open as a page

showing 1–30 of 43