Skip to content
advanced Phase 13 · Federation and Federated Architecture

Composing Subgraphs into a Supergraph

Running composition to merge subgraph schemas and resolve ownership conflicts.

50m
1 problems
Topic Progress 0%

What Composition Actually Merges

What Composition Actually Merges

Composition takes one SDL file per subgraph and produces one supergraph SDL. It is a pure function: the same set of inputs always yields the same output, and if any input pair conflicts it refuses to produce output at all rather than shipping a graph that half works at runtime.

The inputs are plain schemas. The output carries more than clients ever see:

# input: catalog subgraph
type Product @key(fields: "id") { id: ID! name: String! }
type Query { product(id: ID!): Product }

# input: reviews subgraph
type Product @key(fields: "id") { id: ID! reviews: [Review!]! }
type Review { id: ID! rating: Int! }

# output: abridged supergraph SDL
type Product
  @join__type(graph: CATALOG, key: "id")
  @join__type(graph: REVIEWS, key: "id") {
  id: ID!     @join__field(graph: CATALOG) @join__field(graph: REVIEWS)
  name: String! @join__field(graph: CATALOG)
  reviews: [Review!]! @join__field(graph: REVIEWS)
}

Every @join__* annotation is routing data: which subgraph can resolve this field, which keys link them, which graph to call first. Two schemas sit inside that file:

  • The supergraph — everything, join directives included. The gateway reads this.
  • The API schema — the supergraph with join metadata and anything marked @inaccessible stripped away. This is what clients introspect.

That split is why adding a subgraph changes no client contract as long as the API schema is unchanged. It also means you can review ownership changes in a diff without confusing them with breaking API changes: join lines moving are refactors, API lines moving are releases.

The Four Composition Failures You Will See

The Four Composition Failures You Will See

Composition errors name the subgraph, type, and field responsible, so the job is reading them correctly rather than guessing.

Failure What the message means The fix
Field resolved twice two subgraphs resolve the same object field without @shareable pick one owner, or mark @shareable in every subgraph defining it
Unsatisfiable path a field reachable through one subgraph's root field is not resolvable by the subgraph the router must call turn the shared type into an entity with a key both sides declare
Type or default mismatch same field typed Int here and Float there, or argument defaults differ align the definitions, or move the field to one owner
Required input dropped merging input objects by intersection would drop a non-null input field define that input field in every subgraph that needs it

The second one deserves a walkthrough because it is invisible until you read the error. Catalog declares type Position { x: Int! y: Int! } and a root field positionA; inventory declares type Position { y: Int! z: Int! } and positionB. Merging produces a Position with x, y, and z — but a query starting at positionA lands in catalog, which has never heard of z. The router would either error or silently drop the field, so composition refuses:

SATISFIABILITY_ERROR: field Position.z returned by positionA
cannot be resolved by subgraph "catalog"

The rule underneath all four: a merged type must be fully resolvable from every root field that can reach it. Making Position an entity (@key(fields: "id") declared in both subgraphs, with y marked @shareable since both resolve it) satisfies the rule — the router fetches id and x from catalog, then hops to inventory for z.

Composition also merges inputs and arguments by intersection: if the router might pass an argument to a subgraph that does not define it, the merge would drop it, so non-null disagreements fail loudly instead of becoming runtime surprises.

Publishing, Checks, and Ship Order

Publishing, Checks, and Ship Order

Composition is only build-time if somebody feeds it. The reliable shape is: each subgraph publishes its schema on merge, CI composes the proposed set, and the gateway consumes a versioned supergraph artifact.

# publish on every merge to main — the registry becomes the source of truth
rover subgraph publish storefront@prod \
  --name reviews --schema ./reviews/schema.graphql

# CI gate: compose proposed schemas, then diff against live field usage
rover subgraph check storefront@prod \
  --name catalog --schema ./catalog/schema.graphql

Publishing does three things a file in a git repo cannot. It versions each subgraph's schema so an incident from last Tuesday can be reproduced with the exact inputs that shipped. It gives composition a complete input set — a supergraph can only be built from subgraphs that have announced themselves. And it powers checks that compare a proposed change against real traffic: removing a field that still receives 3% of production operations fails the build instead of becoming an outage.

Ship order matters once composition is green. When an entity gains a second key, every subgraph contributing fields to that entity must declare it before composition succeeds — so roll the schema changes out to all owners first, publish them, let CI compose, then let the gateway pick up the new supergraph. Reversing that order produces a gateway that plans against keys half the fleet does not understand.

The discipline that keeps this tractable is monotonic evolution: add fields first, deprecate them in place, remove them only after usage drops to zero. Composition enforces structural compatibility; usage data from checks enforces social compatibility. You need both, because a schema can be perfectly composable and still break the mobile client that stopped sending the argument six months ago.

Practice Problems

0 / 1 solved
Fix an Unsatisfiable Composition Error
Schema Composition

Composition fails: a query entering through catalog.positionA cannot resolve Position.z, which only inventory defines, and both subgraphs define Position.y. Produce corrected subgraph schemas so composition succeeds while keeping every existing root field working.

Example:

Input: # catalog type Position { x: Int! y: Int! } type Query { positionA: Position } # inventory type Position { y: Int! z: Int! } type Query { positionB: Position } ERROR: SATISFIABILITY_ERROR — Position.z returned by positionA cannot be resolved by subgraph "catalog"

Output: # catalog type Position @key(fields: "id") { id: ID! x: Int! y: Int! @shareable } type Query { positionA: Position } # inventory type Position @key(fields: "id") { id: ID! y: Int! @shareable z: Int! } type Query { positionB: Position }

Position becomes an entity, so the router can fetch id/x/y from catalog and hop to inventory for z using the shared key. y is resolved by both subgraphs, which requires @shareable in both. id must exist in both definitions because it is the key.

Optimal Solution — O(1) composition; +1 entity hop at runtime for foreign fields time, O(F) where F is the merged field count space

Promote the shared value type to an entity with a key present in both subgraphs, mark the doubly-defined field @shareable, and let the router reach the remaining fields through an entity hop.

# catalog
type Position @key(fields: "id") { id: ID! x: Int! y: Int! @shareable }
type Query { positionA: Position }

# inventory
type Position @key(fields: "id") { id: ID! y: Int! @shareable z: Int! }
type Query { positionB: Position }

# plan for { positionA { x y z } }:
# fetch catalog { positionA { id x y } }
# fetch inventory representations=[{__typename:"Position", id}] { z }

Edge Cases:

  • Adding z to catalog instead also works, but duplicates the field and forces two resolvers to stay in sync
  • If id itself lives only in one subgraph, the key cannot be declared everywhere — move id into both
  • Marking only one subgraph’s y as @shareable fails composition: the directive must be consistent

Quiz

1. Two subgraphs both resolve Product.price and composition fails with INVALID_FIELD_SHARING. Which fix is valid?

Question 1 options

2. What is the difference between the supergraph and the API schema?

Question 2 options

3. A query starts at catalog.positionA, and the merged Position type has a z field only inventory defines. What does composition do?

Question 3 options

4. Why publish subgraph schemas to a registry instead of committing SDL files and composing from the repo?

Question 4 options

Flashcards

Question

Composition

Answer

Pure build-time merge of subgraph SDLs into one supergraph; succeeds deterministically or fails with subgraph/type/field-level errors.

Question

Supergraph vs API schema

Answer

Supergraph = join routing metadata included (gateway-facing). API schema = supergraph minus joins and @inaccessible (client-facing).

Question

@shareable

Answer

Declares that multiple subgraphs may resolve the same object field; must appear wherever that field is defined.

Question

SATISFIABILITY_ERROR

Answer

A merged field cannot be resolved from a root field that reaches it — fix by making the shared type an entity with a common key.

Question

Schema check

Answer

CI composition of proposed subgraph schemas plus a diff against live field usage; blocks breaking removals before merge.

Revision Notes

Key Takeaways

  • 1. Composition is build-time and deterministic: green CI means the supergraph is safe to ship
  • 2. Every merged type must be resolvable from every root field that reaches it — that is what unsatisfiability catches
  • 3. Publish on merge and gate on checks so composition inputs are versioned and usage-informed

Interview Tips

  • • Read a composition error aloud in three parts: which subgraph, which type and field, which rule was violated
  • • Explain the supergraph/API-schema split as gateway contract versus client contract when asked what clients introspect

Cheat Sheet

Inputs → output

subgraph SDLs (published) --composition--> supergraph SDL (@join__*)
                                            └─ strip joins + @inaccessible --> API schema

Failure cheat sheet

Symptom Cause Fix
INVALID_FIELD_SHARING two resolvers for one field one owner, or @shareable everywhere
SATISFIABILITY_ERROR field unreachable from a root field make the type an entity with a shared @key
type/default mismatch same field, different definitions align or relocate the field
required input dropped intersection removes non-null input define it in every subgraph

Commands

rover subgraph publish graph@variant --name reviews --schema ./schema.graphql
rover subgraph check  graph@variant --name catalog --schema ./schema.graphql

Ship order: add keys/fields in all owning subgraphs → publish → compose → roll the supergraph to the gateway.

Continue Learning