Since 2.3

Entity Interfaces

Add entity fields polymorphically


Apollo Federation provides powerful extensions to GraphQL interfaces, specifically for use with your supergraph's entities:

  1. Apply the @key directive to an interface definition to make it an entity interface.

  2. In other subgraphs, use the @interfaceObject directive to automatically add fields to every entity that implements your entity interface.

With these extensions, your subgraphs can quickly contribute sets of fields to multiple entities, without needing to duplicate any existing (or future) entity definitions.

Overview video

Example schemas

Let's look at a supergraph that defines a Media entity interface, along with a Book entity that implements it:

GraphQL
Subgraph A
1interface Media @key(fields: "id") {
2  id: ID!
3  title: String!
4}
5
6type Book implements Media @key(fields: "id"){
7  id: ID!
8  title: String!
9}
GraphQL
Subgraph B
1type Media @key(fields: "id") @interfaceObject {
2  id: ID!
3  reviews: [Review!]!
4}
5
6type Review {
7  score: Int!
8}
9
10type Query {
11  topRatedMedia: [Media!]!
12}

This example is short, but there's a lot to it. Let's break it down:

  • Subgraph A defines the Media interface, along with the implementing Book entity.

    • The Media interface uses the @key directive, which makes it an entity interface.

    • This usage requires that all objects implementing Media are entities, and that those entities all use the specified @key(s).

    • As shown, Book is an entity and it does use the single specified @key.

  • Subgraph B wants to add a reviews field to every entity that implements Media.

    • To achieve this, Subgraph B also defines Media, but as an object type. Learn why this is necessary.

    • Subgraph B applies the @interfaceObject directive to Media, which indicates that the object corresponds to another subgraph's entity interface.

    • Subgraph B applies the exact same @key(s) to Media that Subgraph A does, and it also defines all @key fields (in this case, just id).

    • Subgraph B defines the new Media.reviews field.

    • Subgraph B will also be responsible for resolving the reviews field. To learn how, see Resolving an @interfaceObject.

When composition runs for the above subgraph schemas, it identifies Subgraph B's @interfaceObject. It adds the new reviews field to the supergraph schema's Media interface, and it also adds that field to the implementing Book entity (along with any others):

GraphQL
Supergraph schema (simplified)
1interface Media @key(fields: "id") {
2  id: ID!
3  title: String!
4  reviews: [Review!]!
5}
6
7type Book implements Media @key(fields: "id"){
8  id: ID!
9  title: String!
10  reviews: [Review!]!
11}
12
13type Review {
14  score: Int!
15}

Subgraph B could have added Book.reviews by contributing the field directly to the entity as usual. However, what if we wanted to add reviews to a hundred different entity implementations of Media?

By instead adding entity fields via @interfaceObject, we can avoid redefining a hundred entities in Subgraph B (not to mention adding more definitions whenever a new implementing entity is created). Learn more.

Requirements

To use entity interfaces and @interfaceObject, your supergraph must adhere to all of the following requirements. Otherwise, composition will fail.

Enabling support

  • If they don't already, all of your subgraph schemas must use the @link directive to enable Federation 2 features.

  • Any subgraph schema that uses the @interfaceObject directive or applies @key to an interface must target v2.3 or later of the Apollo Federation specfication:

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

    Additionally, schemas that use @interfaceObject must include it in the @link directive's import array as shown above.

Usage rules

The interface definition

Let's say Subgraph A defines the MyInterface type as an entity interface so that other subgraphs can add fields to it:

GraphQL
Subgraph A
1interface MyInterface @key(fields: "id") {
2  id: ID!
3  originalField: String!
4}
5
6type MyObject implements MyInterface @key(fields: "id") {
7  id: ID!
8  originalField: String!
9}

In this case:

  • Subgraph A must include at least one @key directive in its MyInterface definition.

    • It may include multiple @keys.

  • Subgraph A must define every entity type in your entire supergraph that implements MyInterface.

    • Certain other subgraphs can also define these entities, but Subgraph A must define all of them.

    • You can think of a subgraph that defines an entity interface as also owning every entity that implements that interface.

  • Subgraph A must be able to uniquely identify any instance of any entity that implements MyInterface, using only the @key fields defined by MyInterface.

    • In other words, if EntityA and EntityB both implement MyInterface, no instance of EntityA can have the exact same values for its @key fields as any instance of EntityB.

    • This uniqueness requirement is always true among instances of a single entity. With entity interfaces, this requirement extends across instances of all implementing entities.

    • This is required to support deterministically resolving the interface in Subgraph A.

  • Every entity that implements MyInterface must include all @keys from the MyInterface definition.

    • These entities can optionally define additional @keys as needed.

@interfaceObject definitions

Let's say Subgraph B applies @interfaceObject to an object type named MyInterface:

GraphQL
Subgraph B
1type MyInterface @key(fields: "id") @interfaceObject {
2  id: ID!
3  addedField: Int!
4}

In this case:

  • At least one other subgraph must define an interface type named MyInterface with the @key directive applied to it (e.g., Subgraph A above)

    GraphQL
    Subgraph A
    1interface MyInterface @key(fields: "id") {
    2  id: ID!
    3  originalField: String!
    4}
  • every subgraph that defines MyInterface as an object type must:

    • Apply @interfaceObject to its definition

    • Include the exact same @key(s) as the interface type's definition

  • Subgraph B must not also define MyInterface as an interface type.

  • Subgraph B must not define any entity that implements MyInterface.

    • If a subgraph contributes entity fields via @interfaceObject, it "gives up" the ability to contribute fields to any individual entity that implements that interface.

Required resolvers

interface reference resolver

In the example schemas above, Subgraph A defines Media as an entity interface, which includes applying the @key directive to it:

GraphQL
Subgraph A
1interface Media @key(fields: "id") {
2  id: ID!
3  title: String!
4}

As it does with any standard entity, @key indicates "this subgraph can resolve any instance of this type if provided its @key fields." This means Subgraph A needs to define a reference resolver for Media, just as it would for any other entity.

 note
The method for defining a reference resolver depends on which subgraph library you use. Some subgraph libraries might use a different term for this functionality. Consult your library's documentation for details.

Here's an example reference resolver for Media if using Apollo Server with the @apollo/subgraph library:

JavaScript
1Media: {
2  __resolveReference(representation) {
3    return allMedia.find((obj) => obj.id === representation.id);
4  },
5},
6
7// ....other resolvers ...

In this example, the hypothetical variable allMedia contains all Media data, including each object's id.

@interfaceObject resolvers

Field resolvers

In the example schemas above, Subgraph B defines Media as an object type and applies @interfaceObject to it. It also defines a Query.topRatedMedia field:

GraphQL
Subgraph B
1type Media @key(fields: "id") @interfaceObject {
2  id: ID!
3  reviews: [Review!]!
4}
5
6type Review {
7  score: Int!
8}
9
10type Query {
11  topRatedMedia: [Media!]!
12}

Subgraph B needs to define a resolver for the new topRatedMedia field, along with any other fields that return the Media type.

Remember: from the perspective of Subgraph B, Media is an object. Therefore, you create resolvers for it using the same sort of logic that you would use for any other object. Subgraph B only needs to be able to resolve the Media fields that it knows about (id and reviews).

Reference resolver

Notice that in Subgraph B, Media is an object type with @key applied. Therefore, it's a standard entity. As with any entity definition, it also requires a corresponding reference resolver:

JavaScript
1Media: {
2  __resolveReference(representation) {
3    return allMedia.find((obj) => obj.id === representation.id);
4  },
5},
6
7// ....other resolvers ...

Why is @interfaceObject necessary?

Without the @interfaceObject directive and its associated composition logic, distributing an interface type's definition across subgraphs can impose continual maintenance requirements on your subgraph teams.

Let's look at an example that doesn't use @interfaceObject. Here, Subgraph A defines the Media interface, along with two implementing entities:

GraphQL
Subgraph A
1interface Media {
2  id: ID!
3  title: String!
4}
5
6type Book implements Media @key(fields: "id") {
7  id: ID!
8  title: String!
9  author: String!
10}
11
12type Movie implements Media @key(fields: "id") {
13  id: ID!
14  title: String!
15  director: String!
16}

Now, if Subgraph B wants to add a reviews field to the Media interface, it can't just define that field:

GraphQL
Subgraph B
1interface Media {
2  reviews: [Review!]!
3}
4
5type Review {
6  score: Int!
7}
8
9type Query {
10  topRatedMedia: [Media!]!
11}

This addition breaks composition. In the supergraph schema, the Media interface now defines the reviews field, but neither Book nor Movie does!

For this to work, Subgraph B also needs to add the reviews field to every entity that implements Media:

⚠️

GraphQL
1interface Media {
2  reviews: [Review!]!
3}
4
5type Review {
6  score: Int!
7}
8
9type Book implements Media @key(fields: "id") {
10  id: ID!
11  reviews: [Review!]!
12}
13
14type Movie implements Media @key(fields: "id") {
15  id: ID!
16  reviews: [Review!]!
17}

This resolves our current composition error, but composition will break again whenever Subgraph A defines a new entity that implements Media:

GraphQL
Subgraph A
1type Podcast implements Media @key(fields: "id") {
2  id: ID!
3  title: String!
4}

To prevent these composition errors, the teams maintaining Subgraph A and Subgraph B need to coordinate their schema changes every time a new implementation of Media is created. Imagine how complex that coordination becomes if the definition of Media is instead distributed across ten subgraphs!

In summary, Subgraph B shouldn't need to know every possible kind of Media that exists in your supergraph. Instead, it should generically know how to fetch reviews for any kind of Media. This is the relationship that entity interfaces and @interfaceObject provide, as demonstrated in the example above.

Are there alternatives to using @interfaceObject?

The primary alternative to using @interfaceObject is to use the discouraged strategy described in the previous section. This requires duplicating all implementations of a given interface in each subgraph that contributes fields to that interface.

Note that this alternative also requires that each subgraph can resolve the type of any object that implements the interface. In many cases, a particular subgraph can't do this, which means this alternative is not feasible.

Feedback

Edit on GitHub

Forums