Docs
Launch Apollo Studio

Schema composition


In Apollo Federation, composition is the process of combining a set of subgraph schemas into a supergraph schema:

(Composition succeeds)
Subgraph
schema
A
Subgraph
schema
B
Subgraph
schema
C
🛠
Composition
Supergraph schema
(A + B + C + routing machinery)

The supergraph schema includes all of the type and field definitions from your subgraph schemas. It's what enables your gateway to intelligently route incoming GraphQL operations across all of your different subgraphs.

Supported methods

You can perform schema composition with any of the following methods:

Manually with the Rover CLI

The Rover CLI supports a supergraph compose command that you can use to compose a supergraph schema from a collection of subgraph schemas:

rover supergraph compose --config ./supergraph-config.yaml

⚠️ Important: To perform Federation 2 composition with Rover, the YAML --config file you provide must include the following line:

supergraph-config.yaml
federation_version: 2

Otherwise, Rover uses Federation 1 composition.

To learn how to install Rover and use this command, see the Quickstart.

Automatically with managed federation

With managed federation, Apollo performs composition automatically whenever one of your subgraphs updates its registered schema. This enables your running gateway to dynamically fetch an updated supergraph schema from Apollo as soon as it's available:

Apollo cloud
Your infrastructure
Publishes schema
Publishes schema
Updates config
Polls for config changes
Apollo Schema
Registry
Apollo
Uplink
Products
subgraph
Reviews
subgraph
Gateway

To learn how to perform composition with managed federation, see the Quickstart.

Breaking composition

Sometimes, your subgraph schemas might conflict in a way that causes composition to fail. This is called breaking composition.

For example, take a look at these two subgraph schemas:

Subgraph A
type Event {
timestamp: String!
}
Subgraph B
type Event {
timestamp: Int!
}

One subgraph defines Event.timestamp as a String, and the other defines it as an Int. Composition doesn't know which type to use, so it fails.

For examples of valid inconsistencies in field return types, see Differing shared field return types.

Breaking composition is a helpful feature of federation! Whenever a team modifies their subgraph schema, those changes might conflict with another subgraph. But that conflict won't affect your gateway, because composition fails to generate a new supergraph schema. It's like a compiler error that prevents you from running invalid code.

Rules of composition

In Federation 2, your subgraph schemas must follow all of these rules to successfully compose into a supergraph schema:

  • Multiple subgraphs can't define the same field on an object type, unless that field is shareable.
  • A shared field must have both a compatible return type and compatible argument types across each defining subgraph.
  • If multiple subgraphs define the same type, each field of that type must be resolvable by every valid GraphQL operation that includes it.

Unresolvable field example

This example presents a field of a shared type that is not always resolvable (and therefore breaks composition).

Consider these subgraph schemas:

Subgraph A
type Query {
positionA: Position!
}
type Position @shareable {
x: Int!
y: Int!
}
Subgraph B
type Query {
positionB: Position!
}
type Position @shareable {
x: Int!
y: Int!
z: Int!
}

Note the following about these two subgraphs:

  • They both define a shared Position type.
  • They both define a top-level Query field that returns a Position.
  • Subgraph B's Position includes a z field, whereas Subgraph A's definition only includes shared x and y fields.

Individually, these subgraph schemas are perfectly valid. However, if they're combined, they break composition. Why?

The composition process attempts to merge inconsistent type definitions into a single definition for the supergraph schema. In this case, the resulting definition for Position exactly matches Subgraph B's definition:

Hypothetical supergraph schema
type Query {
# From A
positionA: Position!
# From B
positionB: Position!
}
type Position {
# From A+B
x: Int!
y: Int!
# From B
z: Int!
}

Based on this hypothetical supergraph schema, the following query should be valid:

query GetPosition {
positionA {
x
y
z # ⚠️ Can't be resolved! ⚠️
}
}

Here's our problem. Only Subgraph A can resolve Query.positionA, because Subgraph B doesn't define the field. But Subgraph A doesn't define Position.z!

If the gateway sent this query to Subgraph A, it would return an error. And without extra configuration, Subgraph B can't resolve a z value for a Position in Subgraph A. Therefore, Position.z is unresolvable for this query.

Composition recognizes this potential issue, and it fails. The hypothetical supergraph schema above would never actually be generated.

Position.z is an example of a field that is not always resolvable. So now, how do we make sure that such a field is always resolvable?

Solutions for unresolvable fields

There are multiple solutions for making sure that a field of a shared type is always resolvable. Choose a solution based on your use case:

Define the field in every subgraph that defines the type.

If every subgraph that defines a type could resolve every field of that type without introducing complexity, a straightforward solution is to define and resolve all fields in all of those subgraphs:

Subgraph A
type Position @shareable {
x: Int!
y: Int!
z: Int
}
Subgraph B
type Position @shareable {
x: Int!
y: Int!
z: Int!
}

In this case, if Subgraph A only cares about the x and y fields, its resolver for z can always return null.

This is a useful solution for shared types that encapsulate simple scalar data.

You can use the @inaccessible directive to incrementally add a value type field to multiple subgraphs without breaking composition. Learn more.

Make the shared type an entity.

Subgraph A
type User @key(fields: "id") {
id: ID!
name: String!
}
Subgraph B
type User @key(fields: "id") {
id: ID!
age: Int!
}

If you make a shared type an entity, different subgraphs can define any number of different fields for that type, as long as they all define key fields for it.

This is a useful solution when a type corresponds closely to an entry in a data store that one or more of your subgraphs has access to (e.g., a Users database).

Edit on GitHub
Previous
Overview
Next
Federated directives