Docs
Launch GraphOS Studio

Value types in Apollo Federation

Share types and fields across multiple subgraphs


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

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

type Position {
x: Int!
y: Int!
}

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

Sharing object types

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

Consider the following Position example:

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

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

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

Using @shareable

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

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

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

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

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

Marking a type as @shareable is equivalent to marking all of its s as @shareable, so the two definitions above are equivalent.

Both s A and B can now resolve the x and y s for the Position type, and our s will successfully compose into a .

⚠️ Important considerations for @shareable

  • If a type or is marked @shareable in any , it must be marked either @shareable or @external in every that defines it. Otherwise, fails.
  • If multiple s can resolve a , make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results depending on which resolves the .

Using @shareable with extend

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

Subgraph A
type Position @shareable {
x: Int! # shareable
y: Int! # shareable
}
extend type Position {
z: Int! # ⚠️ NOT shareable!
}

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 z with @shareable.

    extend type Position {
    z: Int! @shareable
    }
  • 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 multiple times.

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

Differing shared fields

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

Return types

Let's say two s both define an Event with a timestamp :

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

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

Next, look at these varying definitions for the Position :

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

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

Supergraph schema
type Position {
x: Int
y: Int
}

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

Note that A's actual is not modified. Within A, x and y remain non-nullable.

Arguments

s for a shared can differ between s in certain ways:

  • If an is required in at least one , it can be optional in other s. It cannot be omitted.
  • If an is optional in every subgraph where it's defined, it is technically valid to omit it in other s. 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 for that .

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

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

⚠️

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

For more information, see Input types and field arguments.

Omitting fields

Look at these two definitions of a Position :

⚠️

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

B defines a z , but A doesn't. In this case, when generates the Position type for the , it includes all three s:

Supergraph schema
type Position {
x: Int!
y: Int!
z: Int!
}

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

Subgraph A
type Query {
currentPosition: Position!
}

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

query GetCurrentPosition {
currentPosition {
x
y
z # ⚠️ Unresolvable! ⚠️
}
}

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

Composition recognizes this potential problem, and it fails with an error. So how do we fix it? Check out Solutions for unresolvable fields.

Adding new shared fields

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

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

⚠️

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

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

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

Using @inaccessible

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

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

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

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

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

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

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

Note that Position.z does appear in the supergraph schema, but the API schema enforces which s clients can include in s. Learn more about federated schemas.

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

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

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

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

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

now successfully includes Position.z in the !

Unions and interfaces

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

Subgraph A
union Media = Book | Movie
interface User {
name: String!
}
Subgraph B
union Media = Book | Podcast
interface User {
name: String!
age: Int!
}

Compositional logic merges these definitions in your :

Supergraph schema
union Media = Book | Movie | Podcast
# The object types that implement this interface are
# responsible for resolving these fields.
interface User {
name: String!
age: Int!
}

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

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

Challenges with shared interfaces

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

Subgraph A
interface Media {
id: ID!
title: String!
}
type Book implements Media {
id: ID!
title: String!
}
Subgraph B
interface Media {
id: ID!
title: String!
}
type Podcast implements Media {
id: ID!
title: String!
}

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

Subgraph A
interface Media {
id: ID!
title: String!
}
type Book implements Media {
id: ID!
title: String!
# ❌ Doesn't define creator!
}
Subgraph B
interface Media {
id: ID!
title: String!
creator: String!
}
type Podcast implements Media {
id: ID!
title: String!
creator: String!
}

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

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

Solution: Entity interfaces

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

Input types

s can share input type definitions, but merges their s using an intersection strategy. When input types are composed across multiple s, only mutual s are preserved in the :

Subgraph A
input UserInput {
name: String!
age: Int # Not in Subgraph B
}
Subgraph B
input UserInput {
name: String!
email: String # Not in Subgraph A
}

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

Supergraph schema
input UserInput {
name: String!
}

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

Previous
Federated directives
Next
Entities (basics)
Edit on GitHubEditForumsDiscord