skip to content

In Apollo Federation, how does composition merge an enum that two subgraphs define with different values?

level: middleimportance: nice to knowfreq 28%

answer

  1. Direction of travel decides the rule
  2. Values a client can receive
  3. Values every subgraph must accept
  4. Both positions pull opposite ways
  5. Add everywhere before producing

basics

~20 s

It depends where the enum is used. Federation 2 takes the union for an enum used only in output positions, the intersection for one used only in input positions, and exact agreement when it is used both ways.

solid answer

~50 s

The rule follows from direction of travel, so it is worth deriving rather than memorising. An enum in an **output** position is something a client receives, so the published enum must contain every value any subgraph could return — the union — or a client would get a value its schema says is impossible. An enum in an **input** position is something a client sends, so the published enum can only offer values every subgraph that might receive it accepts — the intersection — or a request would fail deep inside one service. When the same enum is used in both positions, those two rules pull in opposite directions and no merge satisfies both, so composition insists the definitions match. Output drift is usually reported as a hint rather than an error, which makes it easy to miss until a client sees an unexpected value.

code

graphql · 15 lines
graphql
# Devices subgraph
enum ReadingUnit { CELSIUS, MILLIMETRES, PERCENT }

type Sensor @key(fields: "id") {
  id: ID!
  readings(unit: ReadingUnit!): [Reading!]!   # input position
}

# Agronomy subgraph
enum ReadingUnit { CELSIUS, MILLIMETRES, PERCENT, KILOPASCALS }

type Reading {
  value: Float!
  unit: ReadingUnit!                           # output position
}

go deeper

for a junior

Know that an enum shared by several services is not yours alone to edit. Adding a value in your subgraph can change or break the merged schema, so treat it as a graph-wide change rather than a local one.

for a middle

Be able to derive the rule instead of reciting it: values a client receives can be widened to a union, values a client sends must be narrowed to what every subgraph accepts, and an enum used both ways has to agree.

for a senior

Show the migration order — declare everywhere, then produce, then consume — and explain why the reverse order stops the whole graph composing and blocks teams who changed nothing.

for a principal

Decide whether widely shared enums should be shared at all. Weigh one enum with a cross-team change protocol against per-domain enums plus mapping, and be clear about who owns the value set and how long a rollout may take.

## Why an enum is the odd one out Most merge conflicts in Apollo Federation are decided by one question: is there a single declaration that is honest for every subgraph? For object fields that question has a clean answer. For an enum it does not, because an enum type is one declaration that can be used in two directions — as the return type of a field, and as the type of an argument or input field — and the safe merge is opposite in the two cases. Take a farm-sensor graph. A Devices subgraph and an Agronomy subgraph both know about units: ```graphql # Devices subgraph enum ReadingUnit { CELSIUS, MILLIMETRES, PERCENT } # Agronomy subgraph — added after a soil-tension rollout enum ReadingUnit { CELSIUS, MILLIMETRES, PERCENT, KILOPASCALS } ``` Neither team did anything wrong. Agronomy shipped a new sensor class and added the value it needs. Devices has no such hardware and never added it. Whether that composes, and into what, depends entirely on where `ReadingUnit` appears. ## Output-only: the union If `ReadingUnit` is only ever returned — `Reading.unit: ReadingUnit!` — then the values flow outward. A client executing against the supergraph may be routed to Agronomy and handed `KILOPASCALS`. If the published enum lacked that value, the client would receive something its generated types and its switch statements say cannot happen, and a strict client would treat it as a protocol violation. So the published enum takes the union: every value any subgraph can return. The practical catch is that this is comfortable enough that composition typically reports the difference as a hint rather than an error. Nobody is blocked, and the drift lives quietly in the graph until a client that only handles three values meets the fourth. ## Input-only: the intersection If `ReadingUnit` is only ever supplied — `readings(unit: ReadingUnit!)` — the values flow inward, and the direction reverses. Publishing `KILOPASCALS` would let a client send it in a request the router may route to Devices, which has never heard of the value. Devices would have to fail on an input its own schema declares invalid. So the published enum keeps only the values every subgraph that could receive it declares: the intersection. A value one team added is simply unavailable to clients until every other subgraph adds it too, which is a real and often surprising blocking dependency. ## Used in both: exact agreement The interesting case is the enum used in both positions, which is very common — the same `ReadingUnit` returned on `Reading.unit` and accepted on a filter argument. Now the union rule and the intersection rule apply to the same declaration, and they disagree the moment the definitions differ. Federation 2 resolves that by refusing: the subgraphs must define the same values, and a difference is a composition error rather than a hint. This is the case people hit in practice and it is why "just add the value in your own service" is bad advice on a shared enum. ## Migrating a shared enum without blocking anyone Because the two directions have opposite safety rules, the sequence matters, and on a 62-subgraph supergraph it is the difference between a two-hour change and a two-week one: 1. Add the new value to every subgraph that declares the enum, in whatever order the teams can manage. Adding a value nobody returns is inert. 2. Only once every declaration carries it, start returning it and start accepting it. 3. Removals run in reverse: stop producing the value everywhere, confirm nothing sends it, then delete it from every subgraph in the same window. Step 1 is the one teams skip. Shipping the producer first is what turns a routine addition into a composition failure that blocks unrelated teams' releases, because until it is fixed nothing new composes at all. ## What to say about specification versus behaviour Be precise about ownership of the rule. The GraphQL specification defines enums and says a value outside the definition is invalid; it says nothing at all about merging two definitions, because it only ever validates one schema. Union, intersection and the both-positions rule come from Apollo Federation's composition specification. Presenting them as "GraphQL behaviour" is the answer a strong interviewer will push back on, and the derivation — outward means union, inward means intersection — is what shows you understand the constraint rather than having memorised a table.

  • Why is an enum value added in one subgraph safe to return but not safe to accept?
    Returning it only affects clients, and the merged schema can be widened to declare it, so nobody is asked to handle something they never declared. Accepting it affects services: the router may route the request to a subgraph whose own schema does not define the value, so it would have to reject an input the graph advertised as valid. Outward is widenable, inward is not.
  • How would you add a value to an enum that a dozen subgraphs declare and that is used both ways?
    In two phases. First every subgraph adds the value to its declaration and ships — inert while nothing produces or accepts it. Only when the last one has landed does any service start returning it or acting on it. Doing it in one phase means the graph stops composing at the moment the first service ships, which blocks every unrelated release too.
  • Is any of this enum merging behaviour defined by the GraphQL specification?
    No. The GraphQL specification defines what an enum is and rejects values outside the definition, but it validates one schema and has no concept of merging several. Union, intersection and the requirement to agree when an enum is used in both positions are Apollo Federation composition rules layered on top.

A returns desk can accept more reasons than the form lists, but the form may only offer reasons every branch will honour.

saying these in an interview costs you the question

  • Says enums must always match exactly across subgraphs
  • Thinks the union applies to argument enums too
  • Attributes the merge rules to the GraphQL specification
  • Ships the producing subgraph before the others declare the value
  • Assumes an output drift error blocks the publish

context