Composition Rules
Learn what rules subgraph schemas must follow to successfully compose
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.
For examples of compatible and incompatible differences between subgraphs, see Differing shared fields.
If multiple subgraphs define the same type, each field of that type must be resolvable by every valid GraphQL operation that includes it.
This rule is the most complex and the most essential to Federation 2. Let's look at it more closely.
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:
❌
1type Query {
2 positionA: Position!
3}
4
5type Position @shareable {
6 x: Int!
7 y: Int!
8}
1type Query {
2 positionB: Position!
3}
4
5type Position @shareable {
6 x: Int!
7 y: Int!
8 z: Int!
9}
Note the following about these two subgraphs:
They both define a shared
Position
type.They both define a top-level
Query
field that returns aPosition
.Subgraph B's
Position
includes az
field, whereas Subgraph A's definition only includes sharedx
andy
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:
❌
1type Query {
2 # From A
3 positionA: Position!
4 # From B
5 positionB: Position!
6}
7
8type Position {
9 # From A+B
10 x: Int!
11 y: Int!
12 # From B
13 z: Int!
14}
Based on this hypothetical supergraph schema, the following query should be valid:
1query GetPosition {
2 positionA {
3 x
4 y
5 z # ⚠️ Can't be resolved! ⚠️
6 }
7}
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 router 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. Refer to the following section for solutions.
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:
✅
1type Position @shareable {
2 x: Int!
3 y: Int!
4 z: Int
5}
1type Position @shareable {
2 x: Int!
3 y: Int!
4 z: Int!
5}
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.
@inaccessible
directive to incrementally add a value type field to multiple subgraphs without breaking composition. Learn more.Make the shared type an entity
✅
1type User @key(fields: "id") {
2 id: ID!
3 name: String!
4}
1type User @key(fields: "id") {
2 id: ID!
3 age: Int!
4}
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 (for example, a Users
database).
Merging types from multiple subgraphs
If a particular GraphQL type is defined differently by different subgraphs, composition uses one of two strategies to merge those definitions: union or intersection.
Union: The supergraph schema includes all parts of all subgraph definitions for the type.
Intersection: The supergraph schema includes only the parts of the type that are present in every subgraph that defines the type.
The merging strategy that composition uses for a particular type depends on the type, as described below.
Object, union, and interface types
Composition always uses the union strategy to merge object, union, and interface types.
Consider the following subgraph schemas:
1type User @key(fields: "id") {
2 id: ID!
3 name: String!
4 email: String!
5}
6
7union Media = Book | Movie
8
9interface BookDetails {
10 title: String!
11 author: String!
12}
1type User @key(fields: "id") {
2 id: ID!
3 age: Int!
4}
5
6union Media = Book | Podcast
7
8interface BookDetails {
9 title: String!
10 numPages: Int
11}
When these subgraph schemas are composed, the composition process merges the three corresponding types by union. This results in the following type definitions in the supergraph schema:
1type User {
2 id: ID!
3 age: Int!
4 name: String!
5 email: String!
6}
7
8union Media = Book | Movie | Podcast
9
10interface BookDetails {
11 title: String!
12 author: String!
13 numPages: Int
14}
Because composition uses the union strategy for these types, subgraphs can contribute distinct parts and guarantee that those parts will appear in the composed supergraph schema.
Input types and field arguments
Composition always uses the intersection strategy to merge input types and field arguments. This ensures that the router never passes an argument to a subgraph that doesn't define that argument.
Consider the following subgraph schemas:
1input UserInput {
2 name: String!
3 age: Int
4}
5
6type Library @shareable {
7 book(title: String, author: String): Book
8}
1input UserInput {
2 name: String!
3 email: String
4}
5
6type Library @shareable {
7 book(title: String, section: String): Book
8}
These subgraphs define different fields for the UserInput
input type, and they define different arguments for the Library.book
field. After composition merges using intersection, the supergraph schema definitions look like this:
1input UserInput {
2 name: String!
3}
4
5type Library {
6 book(title: String): Book
7}
As you can see, the supergraph schema includes only the input fields and arguments that both subgraphs define.
Enums
If an enum definition differs between subgraphs, the composition strategy depends on how the enum is used:
Scenario | Strategy |
---|---|
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 schemas:
1enum Color {
2 RED
3 GREEN
4 BLUE
5}
6
7type Query {
8 favoriteColor: Color
9}
1enum Color {
2 RED
3 GREEN
4 YELLOW
5}
6
7type Query {
8 currentColor: Color
9}
In this case, the Color
enum is used as the return type of at least one object field. Therefore, composition merges the Color
enum by union, so that all possible subgraph return values are valid.
This results in the following type definition in the supergraph schema:
1enum Color {
2 RED
3 GREEN
4 BLUE
5 YELLOW
6}
Intersection
Consider these subgraph schemas:
1enum Color {
2 RED
3 GREEN
4 BLUE
5}
6
7type Query {
8 products(color: Color): [Product]
9}
1enum Color {
2 RED
3 GREEN
4 YELLOW
5}
6
7type Query {
8 images(color: Color): [Image]
9}
In this case, the Color
enum is used as the type of at least one field argument (or input type field). Therefore, composition merges the Color
enum by intersection, so that subgraphs never receive a client-provided enum value that they don't support.
This results in the following type definition in the supergraph schema:
1# BLUE and YELLOW are removed via intersection
2enum Color {
3 RED
4 GREEN
5}
Exact match
Consider these subgraph schemas:
❌
1enum Color {
2 RED
3 GREEN
4 BLUE
5}
6
7type Query {
8 favoriteColor: Color
9}
1enum Color {
2 RED
3 GREEN
4 YELLOW
5}
6
7type Query {
8 images(color: Color): [Image]
9}
In this case, the Color
enum is used as both:
The return type of at least one object field
The type of at least one field argument (or input type field)
Therefore, the definition of the Color
enum must match exactly in every subgraph that defines it. An exact match is the only scenario that enables union and intersection to produce the same result.
The subgraph schemas above do not compose, because their definitions of the Color
enum differ.
Directives
Composition handles a directive differently depending on whether it's an "executable" directive or a "type system" directive.
Executable directives
Executable directives 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 directive definition of directive @lowercase on FIELD
, which a client could use in their query like so:
1query {
2 getSomeData {
3 someField @lowercase
4 }
5}
An executable directive is composed into the supergraph schema only if all of the following conditions are met:
The directive is defined in all subgraphs.
The directive is defined identically in all subgraphs.
The directive is not included in any
@composeDirective
directives.
Type system directives
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 directives are not composed into the supergraph schema, but they can still provide information to the router via the @composeDirective
directive.