Value Types in Apollo Federation

Share types and fields across multiple subgraphs


In a federated graph, it's common to want to reuse a GraphQL type across multiple subgraphs.

For example, suppose you want to define and reuse a generic Position type in different subgraphs:

GraphQL
1type Position {
2  x: Int!
3  y: Int!
4}

Types like this are called value types. This article describes how to share value types and their fields in federated graph, enabling multiple subgraphs to define and resolve them.

Sharing object types

By default, in Federation 2 subgraphs, a single object field can't be defined or resolved by more than one subgraph schema.

Consider the following Position example:

GraphQL
Subgraph A
1type Position {
2  x: Int!
3  y: Int!
4}
GraphQL
Subgraph B
1type Position {
2  x: Int!
3  y: Int!
4}

Attempting to compose these two subgraph schemas together will break composition. The router doesn't know which subgraph is responsible for resolving Position.x and Position.y. To enable multiple subgraphs to resolve these fields, you must first mark that field as @shareable.

 note
As an alternative, if you want Subgraphs A and B to resolve different fields of Position, you can designate the Position type as an entity.

Using @shareable

The @shareable directive enables multiple subgraphs to resolve a particular object field (or set of object fields).

To use @shareable in a subgraph schema, you first need to add the following snippet to that schema to opt in to Federation 2:

GraphQL
1extend schema
2  @link(url: "https://specs.apollo.dev/federation/v2.3",
3        import: ["@key", "@shareable"])

Then you can apply the @shareable directive to an object type, or to individual fields of that type:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4}
GraphQL
Subgraph B
1type Position {
2  x: Int! @shareable
3  y: Int! @shareable
4}
 note
Marking a type as @shareable is equivalent to marking all of its fields as @shareable, so the two subgraph definitions above are equivalent.

Both subgraphs A and B can now resolve the x and y fields for the Position type, and our subgraph schemas will successfully compose into a supergraph schema.

⚠️ Important considerations for @shareable

  • If a type or field is marked @shareable in any subgraph, it must be marked either @shareable or @external in every subgraph that defines it. Otherwise, composition fails.

  • If multiple subgraphs can resolve a field, make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results depending on which subgraph resolves the field.

Using @shareable with extend

If you apply @shareable to an object type declaration, it only applies to the fields within that exact declaration. It does not apply to other declarations for that same type:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int! # shareable
3  y: Int! # shareable
4}
5
6extend type Position {
7  z: Int! # ⚠️ NOT shareable!
8}

Using the extend keyword, the schema above includes two different declarations for Position. Because only the first declaration is marked @shareable, Position.z is not considered shareable.

To make Position.z shareable, you can do one of the following:

  • Mark the individual field z with @shareable.

    GraphQL
    1extend type Position {
    2  z: Int! @shareable
    3}
  • Mark the entire extend declaration with @shareable.

    • This strategy requires targeting v2.2 or later of the Apollo Federation specification in your subgraph schema. Earlier versions do not support applying @shareable to the same object type multiple times.

    GraphQL
    1extend schema
    2  @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"])
    3
    4extend type Position @shareable {
    5  z: Int!
    6}

Differing shared fields

Shared fields can only differ in their return types and arguments in specific ways. If fields you want to share between subgraphs differ more than is permitted, use entities instead of shareable value types.

Return types

Let's say two subgraphs both define an Event object type with a timestamp field:

GraphQL
Subgraph A
1type Event @shareable {
2  timestamp: Int!
3}
GraphQL
Subgraph B
1type Event @shareable {
2  timestamp: String!
3}

Subgraph A's timestamp returns an Int, and Subgraph B's returns a String. This is invalid. When composition attempts to generate an Event type for the supergraph schema, it fails due to an unresolvable conflict between the two timestamp field definitions.

Next, look at these varying definitions for the Position object type:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int
3  y: Int
4}

The x and y fields are non-nullable in Subgraph A, but they're nullable in Subgraph B. This is valid. Composition recognizes that it can use the following definition for Position in the supergraph schema:

GraphQL
Supergraph schema
1type Position {
2  x: Int
3  y: Int
4}

This definition works for querying Subgraph A, because Subgraph A's definition is more restrictive than this (a non-nullable value is always valid for a nullable field). In this case, composition coerces Subgraph A's Position fields to satisfy the reduced restrictiveness of Subgraph B.

 note
Subgraph A's actual subgraph schema is not modified. Within Subgraph A, x and y remain non-nullable.

Arguments

Arguments for a shared field can differ between subgraphs in certain ways:

  • If an argument is required in at least one subgraph, it can be optional in other subgraphs. It cannot be omitted.

  • If an argument is optional in every subgraph where it's defined, it is technically valid to omit it in other subgraphs. However:

    • ⚠️ If a field argument is omitted from any subgraph, that argument is omitted from the supergraph schema entirely. This means that clients can't provide the argument for that field.

GraphQL
Subgraph A
1type Building @shareable {
2  # Argument is required
3  height(units: String!): Int!
4}
GraphQL
Subgraph B
1type Building @shareable {
2  # Argument can be optional
3  height(units: String): Int!
4}

GraphQL
Subgraph A
1type Building @shareable {
2  # Argument is required
3  height(units: String!): Int!
4}
GraphQL
Subgraph B
1type Building @shareable {
2  # ⚠️ Argument can't be omitted! ⚠️
3  height: Int!
4}

⚠️

GraphQL
Subgraph A
1type Building @shareable {
2  # Argument is optional
3  height(units: String): Int!
4}
GraphQL
Subgraph B
1type Building @shareable {
2  # Argument can be omitted, BUT
3  # it doesn't appear in the
4  # supergraph schema!
5  height: Int!
6}

For more information, see Input types and field arguments.

Omitting fields

Look at these two definitions of a Position object type:

⚠️

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int!
5}

Subgraph B defines a z field, but Subgraph A doesn't. In this case, when composition generates the Position type for the supergraph schema, it includes all three fields:

GraphQL
Supergraph schema
1type Position {
2  x: Int!
3  y: Int!
4  z: Int!
5}

This definition works for Subgraph B, but it presents a problem for Subgraph A. Let's say Subgraph A defines the following Query type:

GraphQL
Subgraph A
1type Query {
2  currentPosition: Position!
3}

According to the hypothetical supergraph schema, the following query is valid against the supergraph:

GraphQL
1query GetCurrentPosition {
2  currentPosition {
3    x
4    y
5    z # ⚠️ Unresolvable! ⚠️
6  }
7}

And here's the problem: if Subgraph B doesn't define Query.currentPosition, this query must be executed on Subgraph A. But Subgraph A is missing the Position.z field, so that field is unresolvable!

Composition recognizes this potential problem, and it fails with an error. To learn how to fix it, check out Solutions for unresolvable fields.

Adding new shared fields

Adding a new field to a value type can cause composition issues, because it's challenging to add the field to all defining subgraphs at the same time.

Let's say we're adding a z field to our Position value type, and we start with Subgraph A:

⚠️

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int!
5}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int!
3  y: Int!
4}

It's likely that when we attempt to compose these two schemas, composition will fail, because Subgraph B can't resolve Position.z.

To incrementally add the field to all of our subgraphs without breaking composition, we can use the @inaccessible directive.

Using @inaccessible

If you apply the @inaccessible directive to a field, composition omits that field from your router's API schema. This helps you incrementally add a field to multiple subgraphs without breaking composition.

To use @inaccessible in a subgraph, first make sure you include it in the import array of your Federation 2 opt-in declaration:

GraphQL
Subgraph A
1extend schema
2  @link(url: "https://specs.apollo.dev/federation/v2.3",
3        import: ["@key", "@shareable", "@inaccessible"])

Then, whenever you add a new field to a value type, apply @inaccessible to that field if it isn't yet present in every subgraph that defines the value type:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int! @inaccessible
5}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int!
3  y: Int!
4}

Even if Position.z is defined in multiple subgraphs, you only need to apply @inaccessible in one subgraph to omit it. In fact, you might want to apply it in only one subgraph to simplify removing it later.

With the syntax above, composition omits Position.z from the generated API schema, and the resulting Position type includes only x and y fields.

 note
Notice that Position.z does appear in the supergraph schema, but the API schema enforces which fields clients can include in operations. Learn more about federated schemas.

Whenever you're ready, you can now add Position.z to Subgraph B:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int! @inaccessible
5}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int!
5}

At this point, Position.z is still @inaccessible, so composition continues to ignore it.

Finally, when you've added Position.z to every subgraph that defines Position, you can remove @inaccessible from Subgraph A:

GraphQL
Subgraph A
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int!
5}
GraphQL
Subgraph B
1type Position @shareable {
2  x: Int!
3  y: Int!
4  z: Int!
5}

Composition now successfully includes Position.z in the supergraph schema!

Unions and interfaces

In Federation 2, union and interface type definitions can be shared between subgraphs by default, and those definitions can differ:

GraphQL
Subgraph A
1union Media = Book | Movie
2
3interface User {
4  name: String!
5}
GraphQL
Subgraph B
1union Media = Book | Podcast
2
3interface User {
4  name: String!
5  age: Int!
6}

Compositional logic merges these definitions in your supergraph schema:

GraphQL
Supergraph schema
1union Media = Book | Movie | Podcast
2
3# The object types that implement this interface are
4# responsible for resolving these fields.
5interface User {
6  name: String!
7  age: Int!
8}

This can be useful when different subgraphs are responsible for different subsets of a particular set of related types or values.

 note
You can also use the enum type across multiple subgraphs. For details, see Merging types from multiple subgraphs.

Challenges with shared interfaces

Sharing an interface type across subgraphs introduces maintenance challenges whenever that interface changes. Consider these subgraphs:

GraphQL
Subgraph A
1interface Media {
2  id: ID!
3  title: String!
4}
5
6type Book implements Media {
7  id: ID!
8  title: String!
9}
GraphQL
Subgraph B
1interface Media {
2  id: ID!
3  title: String!
4}
5
6type Podcast implements Media {
7  id: ID!
8  title: String!
9}

Now, let's say Subgraph B adds a creator field to the Media interface:

GraphQL
Subgraph A
1interface Media {
2  id: ID!
3  title: String!
4}
5
6type Book implements Media {
7  id: ID!
8  title: String!
9  # ❌ Doesn't define creator!
10}
GraphQL
Subgraph B
1interface Media {
2  id: ID!
3  title: String!
4  creator: String!
5}
6
7type Podcast implements Media {
8  id: ID!
9  title: String!
10  creator: String!
11}

This breaks composition, because Book also implements Media but doesn't define the new creator field.

To prevent this error, all implementing types across all subgraphs need to be updated to include all fields of Media. This becomes more and more challenging to do as your number of subgraphs and teams grows. Fortunately, there's a solution.

Solution: Entity interfaces

Apollo Federation 2.3 introduces a powerful abstraction mechanism for interfaces, enabling you to add interface fields across subgraphs without needing to update every single implementing type.

Learn more about entity interfaces.

Input types

Subgraphs can share input type definitions, but composition merges their fields using an intersection strategy. When input types are composed across multiple subgraphs, only mutual fields are preserved in the supergraph schema:

GraphQL
Subgraph A
1input UserInput {
2  name: String!
3  age: Int # Not in Subgraph B
4}
GraphQL
Subgraph B
1input UserInput {
2  name: String!
3  email: String # Not in Subgraph A
4}

Compositional logic merges only the fields that all input types have in common. To learn more, see Merging input types and field arguments.

GraphQL
Supergraph schema
1input UserInput {
2  name: String!
3}

To learn more about how composition merges different schema types under the hood, see Merging types during composition.

Feedback

Edit on GitHub

Forums