Entity Interfaces
Add entity fields polymorphically
Apollo Federation provides powerful extensions to GraphQL interfaces, specifically for use with your supergraph's entities:
- Apply the
@key
directive to aninterface
definition to make it an entity interface. - 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:
interface Media @key(fields: "id") {id: ID!title: String!}type Book implements Media @key(fields: "id"){id: ID!title: String!}
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 implementingBook
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
.
- The
- Subgraph B wants to add a
reviews
field to every entity that implementsMedia
.- To achieve this, Subgraph B also defines
Media
, but as an object type. Learn why this is necessary. - Subgraph B applies the
@interfaceObject
directive toMedia
, which indicates that the object corresponds to another subgraph's entity interface. - Subgraph B applies the exact same
@key
(s) toMedia
that Subgraph A does, and it also defines all@key
fields (in this case, justid
). - 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
.
- To achieve this, Subgraph B also defines
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):
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!}
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 aninterface
must target v2.3 or later of the Apollo Federation 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'simport
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:
interface MyInterface @key(fields: "id") {id: ID!originalField: String!}type MyObject implements MyInterface @key(fields: "id") {id: ID!originalField: String!}
In this case:
- Subgraph A must include at least one
@key
directive in itsMyInterface
definition.- It may include multiple
@key
s.
- It may include multiple
- 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 byMyInterface
.- In other words, if
EntityA
andEntityB
both implementMyInterface
, no instance ofEntityA
can have the exact same values for its@key
fields as any instance ofEntityB
. - 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.
- In other words, if
- Every entity that implements
MyInterface
must include all@key
s from theMyInterface
definition.- These entities can optionally define additional
@key
s as needed.
- These entities can optionally define additional
@interfaceObject
definitions
Let's say Subgraph B applies @interfaceObject
to an object type named MyInterface
:
type MyInterface @key(fields: "id") @interfaceObject {id: ID!addedField: Int!}
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)Subgraph Ainterface MyInterface @key(fields: "id") {id: ID!originalField: String!}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
- Apply
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.
- If a subgraph contributes entity fields via
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:
interface Media @key(fields: "id") {id: ID!title: String!}
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:
Media: {__resolveReference(representation) {return allMedia.find((obj) => obj.id === representation.id);},},// ....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:
type Media @key(fields: "id") @interfaceObject {id: ID!reviews: [Review!]!}type Review {score: Int!}type Query {topRatedMedia: [Media!]!}
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:
Media: {__resolveReference(representation) {return allMedia.find((obj) => obj.id === representation.id);},},// ....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:
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 Subgraph B wants to add a reviews
field to the Media
interface, it can't just define that field:
❌
interface Media {reviews: [Review!]!}type Review {score: Int!}type Query {topRatedMedia: [Media!]!}
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
:
⚠️
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 composition error, but composition will break again whenever Subgraph A defines a new entity that implements Media
:
type Podcast implements Media @key(fields: "id") {id: ID!title: String!}
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.