Docs
Launch GraphOS Studio

Schema composition

In Apollo Federation


In , 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 includes all of the type and definitions from your . It also includes metadata that enables your to intelligently route incoming across all of your different subgraphs.

Supported methods

You can perform schema with any of the following methods:

Automatically with GraphOS

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

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

To learn how to perform with , see the quickstart.

also provides a schema linter with composition specific rules to help you follow best practices. You can set up for your in or perform one-off linting with the . Check out the schema linting docs to learn more.

Manually with the Rover CLI

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

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

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

Breaking composition

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

For example, take a look at these two :

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

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

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

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

Rules of composition

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

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

Unresolvable field example

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

Consider these :

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 :

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

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

The process attempts to merge inconsistent type definitions into a single definition for the . In this case, the resulting definition for Position exactly matches 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 , the following should be valid:

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

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

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

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

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

Solutions for unresolvable fields

There are multiple solutions for making sure that a 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 that defines a type could resolve every of that type without introducing complexity, a straightforward solution is to define and resolve all fields in all of those :

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 A only cares about the x and y , its for z can always return null.

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

You can use the @inaccessible to incrementally add a value type to multiple without breaking . 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 can define any number of different 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 has access to (e.g., a Users database).

Merging types from multiple subgraphs

If a particular type is defined differently by different , uses one of two strategies to merge those definitions: union or intersection.

  • Union: The includes all parts of all definitions for the type.
  • Intersection: The includes only the parts of the type that are present in every that defines the type.

The merging strategy that uses for a particular type depends on the type, as described below.

Object, union, and interface types

always uses the union strategy to merge object, union, and interface types.

Consider the following :

Subgraph A
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
union Media = Book | Movie
interface BookDetails {
title: String!
author: String!
}
Subgraph B
type User @key(fields: "id") {
id: ID!
age: Int!
}
union Media = Book | Podcast
interface BookDetails {
title: String!
numPages: Int
}

When these are composed, the process merges the three corresponding types by union. This results in the following type definitions in the :

Supergraph schema
type User {
id: ID!
age: Int!
name: String!
email: String!
}
union Media = Book | Movie | Podcast
interface BookDetails {
title: String!
author: String!
numPages: Int
}

Because uses the union strategy for these types, can contribute distinct parts and guarantee that those parts will appear in the composed .

Note that if different contribute different to an interface type, any that implement that interface must define all contributed from all . Otherwise, fails.

Input types and field arguments

always uses the intersection strategy to merge input types and . This ensures that the never passes an to a that doesn't define that argument.

Consider the following :

Subgraph A
input UserInput {
name: String!
age: Int
}
type Library @shareable {
book(title: String, author: String): Book
}
Subgraph B
input UserInput {
name: String!
email: String
}
type Library @shareable {
book(title: String, section: String): Book
}

These define different for the UserInput input type, and they define different for the Library.book . After merges using intersection, the definitions look like this:

Supergraph schema
input UserInput {
name: String!
}
type Library {
book(title: String): Book
}

As you can see, the includes only the input and that both define.

⚠️ Important: If the intersection strategy would omit an input or that is non-nullable, composition fails. This is because at least one requires that or , and the can't provide it if it's omitted from the .

When defining input types and in multiple , make sure that every non-nullable field and argument is consistent in every subgraph. For examples, see Arguments.

Enums

If an enum definition differs between , the composition strategy depends on how the enum is used:

ScenarioStrategy
The enum is used as the return type for at least one object or interface field.Union
The enum is used as the type for at least one field argument or input type field.Intersection
Both of the above are true.All definitions must match exactly

Examples of these scenarios are provided below.

Enum composition examples

Union

Consider these :

Subgraph A
enum Color {
RED
GREEN
BLUE
}
type Query {
favoriteColor: Color
}
Subgraph B
enum Color {
RED
GREEN
YELLOW
}
type Query {
currentColor: Color
}

In this case, the Color enum is used as the return type of at least one object . Therefore, merges the Color enum by union, so that all possible return values are valid.

This results in the following type definition in the :

Supergraph schema
enum Color {
RED
GREEN
BLUE
YELLOW
}
Intersection

Consider these :

Subgraph A
enum Color {
RED
GREEN
BLUE
}
type Query {
products(color: Color): [Product]
}
Subgraph B
enum Color {
RED
GREEN
YELLOW
}
type Query {
images(color: Color): [Image]
}

In this case, the Color enum is used as the type of at least one (or input type field). Therefore, merges the Color enum by intersection, so that never receive a client-provided enum value that they don't support.

This results in the following type definition in the :

Supergraph schema
# BLUE and YELLOW are removed via intersection
enum Color {
RED
GREEN
}
Exact match

Consider these :

Subgraph A
enum Color {
RED
GREEN
BLUE
}
type Query {
favoriteColor: Color
}
Subgraph B
enum Color {
RED
GREEN
YELLOW
}
type Query {
images(color: Color): [Image]
}

In this case, the Color enum is used as both:

  • The return type of at least one object
  • The type of at least one (or input type field)

Therefore, the definition of the Color enum must match exactly in every that defines it. An exact match is the only scenario that enables union and intersection to produce the same result.

The above do not compose, because their definitions of the Color enum differ.

Directives

handles a differently depending on whether it's an "executable" directive or a "" directive.

Executable directives

Executable are intended to be used by clients in their queries. They are applied to one or more of the executable directive locations. For example, you might have a definition of directive @lowercase on FIELD, which a client could use in their like so:

query {
getSomeData {
someField @lowercase
}
}

An executable is composed into the only if all of the following conditions are met:

  • The is defined in all .
  • The is defined identically in all .
  • The is not included in any @composeDirective .

Type system directives

help define the structure of the schema and are not intended for use by clients. They are applied to one or more of the type system directive locations.

These are not composed into the , but they can still provide information to the via the @composeDirective .

Previous
Overview
Next
Federated directives
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company