skip to content

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

level: seniorimportance: must knowfreq 46%

answer

  1. Every schema valid, graph still broken
  2. Reachability, not declaration conflict
  3. The router needs a key to jump
  4. Some routes work, others cannot
  5. Caught at build, not in production

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.

solid answer

~50 s

Composition does two jobs. First it merges declarations, catching type and argument conflicts. Then it asks a harder, whole-graph question: for every field in the supergraph, and every path a client could take to reach it, can the router build a plan? If a field lives in a subgraph that can only be entered by a key that no other subgraph can supply, the answer is no, and composition reports a satisfiability error naming the path it could not complete. Nothing is wrong with any single schema — the failure only exists when they are read together, which is why it surprises the team that shipped it. The usual causes are a subgraph declaring an entity with a key nobody else exposes, a type used across services that was never made an entity, and a field whose declared dependency on sibling fields cannot be met on some route into it.

code

graphql · 13 lines
graphql
# Devices subgraph — the entry point clients use
type Query { sensor(id: ID!): Sensor }

type Sensor @key(fields: "id") {
  id: ID!
  model: String!
}

# Agronomy subgraph — keyed on something Devices cannot supply
type Sensor @key(fields: "serialNo") {
  serialNo: String!
  soilMoisturePct: Float!
}

go deeper

for a junior

Know that a field can only be fetched from another service if that service declares an identity the graph can hand it. Adding a field to a shared type in your own subgraph is a graph-level change, not a local one.

for a middle

Explain the mechanics: a plan hops between subgraphs by supplying key fields, so a target that declares no usable key is unreachable and composition proves that before the graph ships.

for a senior

Diagnose one out loud. Read the reported path backwards, identify which move was impossible, and name the three usual causes — mismatched keys, a shared type that is not an entity, and a field dependency that cannot be met on some route.

for a principal

Own the key discipline that prevents them: one canonical identifier per entity that every subgraph touching it can resolve, and a rule about which team may add fields to an entity it cannot independently identify.

## Two different checks wear the name "composition error" The first check is a merge check: two subgraphs declare the same field with different types, or an enum drifts, or an argument list disagrees. Those are local, and the error points at two declarations you can put side by side. Satisfiability is the second check, and it is global. Once the merge succeeds, composition still has to prove that the graph it just built is actually executable: for every field in the supergraph, and for every route a client could take to arrive at it, there must exist a query plan that reaches it. That proof can fail even though every subgraph is a flawless GraphQL schema, and even though no two declarations conflict. It is the check that catches a graph which is well-typed but not traversable. ## How the router moves between subgraphs A plan starts at a root field in one subgraph. To continue into another subgraph it has to hand that service an identity it can look up — the fields named in a `@key` the target subgraph declares on the type. If the target subgraph declares no key the router can build, or declares one whose fields the current subgraph cannot produce, the jump does not exist. There is no fallback: the router will not query a service on an identity that service never declared it could resolve. A farm-sensor graph shows it in three lines of drift. A Devices subgraph owns the hardware: ```graphql # Devices subgraph type Sensor @key(fields: "id") { id: ID! model: String! } ``` An Agronomy team then adds soil moisture. Their store is keyed by the manufacturer's serial number, so they write what is natural in their own repository: ```graphql # Agronomy subgraph type Sensor @key(fields: "serialNo") { serialNo: String! soilMoisturePct: Float! } ``` Both files are valid. Both services start and pass their own tests. But a client asking `sensor(id: "snsr-40917") { soilMoisturePct }` starts in Devices, which has an `id` and no `serialNo`, and Agronomy will only be entered with a `serialNo`. No plan exists, so `Sensor.soilMoisturePct` is unreachable from that route and composition refuses the supergraph. ## Why it fails at composition time rather than at runtime A graph like the one above is not broken for *every* query. If Agronomy also had a root field returning sensors, the field would be reachable by that route and unreachable by the other. Allowing that would mean shipping a schema where a field's availability depends on how the client happened to navigate to it — the same selection succeeding under one parent and failing under another, discovered by a client in production rather than by the team at build time. Federation composition rejects that class of graph up front instead, which is the same fail-closed instinct behind refusing to emit a partial supergraph. ## Reading the error on a large graph On a 62-subgraph supergraph the message is long, and the useful habit is to read it backwards. It states the field that could not be reached, then the path the planner tried, then the reason each candidate move was rejected — typically that the type has no key it could use in the target subgraph, or that a field's declared dependency on sibling fields cannot be satisfied on that route. Three things then explain almost every occurrence: - **Key mismatch.** Two subgraphs key the same entity on fields neither can give the other. The fix is a key both sides can produce — usually adding the graph's canonical identifier to the newcomer's declaration, backed by a lookup in their store. - **A shared type that was never made an entity.** A type declared in two subgraphs with no key at all cannot be jumped into. Either it becomes an entity with a key, or one subgraph stops declaring the extra fields and reaches them another way. - **An unmet field dependency.** A field declared to depend on sibling fields owned elsewhere is only reachable where those siblings can be fetched on the way. Add the missing route or move the field to the service that already holds what it needs. ## The judgement an interviewer is listening for Two things. First, that you can say why the check exists at all — a merged schema being well-typed says nothing about being executable, and satisfiability is the property that fills the gap. Second, that you treat it as a graph design signal rather than a build annoyance. A satisfiability failure usually means a team put a field on an entity their service cannot independently identify, and the durable fix is a key discipline — one canonical identifier per entity that every subgraph touching it can resolve — not another directive bolted on until the build goes green. It is also worth being precise that none of this is the GraphQL specification: reachability across services is a federation composition concern layered on top of a specification that only ever validates one schema.

  • Why not compose anyway and let the field error at runtime when no plan exists?
    Because availability would then depend on the route a client took: the same selection would work under one parent and fail under another, and the team that caused it would learn about it from a client incident. Composition rejecting the graph moves that discovery to the build, which is the same reason it refuses to publish a supergraph with the field quietly removed.
  • How do you fix a satisfiability error caused by two subgraphs keying the same entity differently?
    Give them a key both sides can produce, normally the graph's canonical identifier, and have the newer subgraph resolve on it — which usually means storing that identifier alongside its own. Adding a second key to one subgraph is fine; entities may declare several. What does not work is leaving each service on its own identifier and hoping the router bridges them.
  • Can a satisfiability failure appear when no subgraph schema changed?
    Yes — removing a route is enough. Delete a root field, drop a key from a subgraph, or stop a service publishing a type it used to declare, and paths that other teams' fields depended on disappear. Nothing in the remaining declarations is wrong; the graph simply lost the edge the planner was using, which is why composition runs over the whole set on every publish.

Two rail networks can each be perfectly built and still leave a station unreachable, because no line joins them at a gauge both can run on.

saying these in an interview costs you the question

  • Thinks a satisfiability error is a runtime failure
  • Says each subgraph validating means the graph composes
  • Believes the router can join services on any matching field
  • Assumes it means a subgraph is unreachable on the network
  • Fixes it by deleting the field rather than adding a usable key
  • Confuses it with a type conflict between two declarations

context