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

.

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

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

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

Sharing object types

By default in

, 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 together will

. The doesn't know which is responsible for resolving Position.x and Position.y. To enable multiple to resolve these , you must first mark that field as
@shareable
.

NOTE

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

.

Using @shareable

The @shareable enables multiple 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

:

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 of that type:

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

NOTE

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

Both A and B can now resolve the x and y for the Position type, and our 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 can resolve a , make sure each subgraph's 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 declaration, it only applies to the 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 specification in your . 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 can only differ in their

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

Return types

Let's say two 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 are non-nullable in A, but they're nullable in Subgraph 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 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 Subgraph A's Position to satisfy the reduced restrictiveness of B.

NOTE

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

Arguments

for a shared can differ between in certain ways:

  • If an is required in at least one , it can be optional in other subgraphs. It cannot be omitted.
  • If an is optional in every 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.

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

.

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 :

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

This definition works for B, but it presents a problem for Subgraph A. Let's say Subgraph 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!

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

.

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 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 without breaking , we can use the

.

Using @inaccessible

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

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 , you only need to apply @inaccessible in one to omit it. In fact, you might want to apply it in only one subgraph to simplify removing it later.

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

NOTE

Notice that Position.z does appear in the , but the enforces which clients can include in .

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 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!
}

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 are responsible for different subsets of a particular set of related types or values.

NOTE

You can also use the enum type across multiple . For details, see

.

Challenges with shared interfaces

Sharing an interface type across 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 need to be updated to include all of Media. This becomes more and more challenging to do as your number of and teams grows. Fortunately, there's a

.

Solution: Entity interfaces

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

Input types

can share input type definitions, but merges their using an intersection strategy. When input types are composed across multiple , only mutual 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
}

logic merges only the that all input types have in common. To learn more, see

.

Supergraph schema
input UserInput {
name: String!
}

To learn more about how merges different schema types under the hood, see

.

Previous
Federated directives
Next
Entities (basics)
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company