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 , use the @interfaceObject to automatically add to every that implements your entity interface.

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

Overview video

Example schemas

Let's look at a that defines a Media interface, along with a Book 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 .
    • 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 and it does use the single specified @key.
  • Subgraph B wants to add a reviews to every 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 interface.
    • B applies the exact same @key(s) to Media that A does, and it also defines all @key (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 , 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 (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 as usual. However, what if we wanted to add reviews to a hundred different implementations of Media?

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

Requirements

To use 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 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 interface so that other can add 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 type in your entire supergraph that implements MyInterface.
    • Certain other can also define these entities, but A must define all of them.
    • You can think of a that defines an interface as also owning every that implements that interface.
  • A must be able to uniquely identify any instance of any that implements MyInterface, using only the @key 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 as any instance of EntityB.
    • This uniqueness requirement is always true among instances of a single . With entity interfaces, this requirement extends across instances of all implementing entities.
    • This is required to support deterministically resolving the interface in A.
  • Every 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 that implements MyInterface.

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

Required resolvers

interface reference resolver

In the example schemas above, A defines Media as an 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 ." This means A needs to define a reference resolver for Media, just as it would for any other .

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 that return the Media type.

Remember: from the perspective of B, Media is an object! Therefore, you create 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 that it knows about (id and reviews).

Reference resolver

Notice that in B, Media is an with @key applied. Therefore, it's a standard ! 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 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 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 !

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

© 2024 Apollo Graph Inc.

Privacy Policy

Company