Docs
Launch GraphOS Studio
Since 2.3

Entity interfaces

Add entity fields polymorphically


provides powerful to interfaces, specifically for use with your 's entities:

  1. Apply the @key to an interface definition to make it an entity interface.
  2. In other s, use the @interfaceObject to automatically add s to every entity that implements your entity interface.

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

Overview video

Example schemas

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

Subgraph A
interface Media @key(fields: "id") {
id: ID!
title: String!
}
type Book implements Media @key(fields: "id"){
id: ID!
title: String!
}
Subgraph B
type Media @key(fields: "id") @interfaceObject {
id: ID!
reviews: [Review!]!
}
type Review {
score: Int!
}
type Query {
topRatedMedia: [Media!]!
}

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 , 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 to every entity that implements Media.
    • To achieve this, B also defines Media, but as an object type! Learn why this is necessary.
    • B applies the @interfaceObject to Media, which indicates that the object corresponds to another 's entity interface.
    • B applies the exact same @key(s) to Media that A does, and it also defines all @key s (in this case, just id).
    • B defines the new Media.reviews .
    • B will also be responsible for resolving the reviews . To learn how, see Resolving an @interfaceObject.

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

Supergraph schema (simplified)
interface Media @key(fields: "id") {
id: ID!
title: String!
reviews: [Review!]!
}
type Book implements Media @key(fields: "id"){
id: ID!
title: String!
reviews: [Review!]!
}
type Review {
score: Int!
}

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 s via @interfaceObject, we can avoid redefining a hundred entities in B (not to mention adding more definitions whenever a new implementing entity is created). Learn more.

Requirements

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

Enabling support

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

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

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

    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 A defines the MyInterface type as an entity interface so that other s can add s to it:

Subgraph A
interface MyInterface @key(fields: "id") {
id: ID!
originalField: String!
}
type MyObject implements MyInterface @key(fields: "id") {
id: ID!
originalField: String!
}

In this case:

  • A must include at least one @key in its MyInterface definition.
    • It may include multiple @keys.
  • A must define every entity type in your entire supergraph that implements MyInterface.
    • Certain other s can also define these entities, but A must define all of them.
    • You can think of a that defines an entity interface as also owning every entity that implements that interface.
  • A must be able to uniquely identify any instance of any entity that implements MyInterface, using only the @key s 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 s 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 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 B applies @interfaceObject to an named MyInterface:

Subgraph B
type MyInterface @key(fields: "id") @interfaceObject {
id: ID!
addedField: Int!
}

In this case:

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

    Subgraph A
    interface MyInterface @key(fields: "id") {
    id: ID!
    originalField: String!
    }
  • Every that defines MyInterface as an must:

    • Apply @interfaceObject to its definition
    • Include the exact same @key(s) as the interface type's definition
  • B must not also define MyInterface as an interface type.

  • B must not define any entity that implements MyInterface.

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

Required resolvers

interface reference resolver

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

Subgraph A
interface Media @key(fields: "id") {
id: ID!
title: String!
}

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

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

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

Media: {
__resolveReference(representation) {
return allMedia.find((obj) => obj.id === representation.id);
},
},
// ....other resolvers ...

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

@interfaceObject resolvers

Field resolvers

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

Subgraph B
type Media @key(fields: "id") @interfaceObject {
id: ID!
reviews: [Review!]!
}
type Review {
score: Int!
}
type Query {
topRatedMedia: [Media!]!
}

B needs to define a for the new topRatedMedia , along with any other s that return the Media type.

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

Reference resolver

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

Media: {
__resolveReference(representation) {
return allMedia.find((obj) => obj.id === representation.id);
},
},
// ....other resolvers ...

Why is @interfaceObject necessary?

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

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

Subgraph A
interface Media {
id: ID!
title: String!
}
type Book implements Media @key(fields: "id") {
id: ID!
title: String!
author: String!
}
type Movie implements Media @key(fields: "id") {
id: ID!
title: String!
director: String!
}

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

Subgraph B
interface Media {
reviews: [Review!]!
}
type Review {
score: Int!
}
type Query {
topRatedMedia: [Media!]!
}

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

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

⚠️

interface Media {
reviews: [Review!]!
}
type Review {
score: Int!
}
type Book implements Media @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Movie implements Media @key(fields: "id") {
id: ID!
reviews: [Review!]!
}

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

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

To prevent these errors, the teams maintaining 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 s!

In summary, B shouldn't need to know every possible kind of Media that exists in your . 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 that contributes s to that interface.

Note that this alternative also requires that each 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.

Previous
Entities (advanced)
Next
Migrating from schema stitching
Edit on GitHubEditForumsDiscord