skip to content

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