Here's your chance to speak at GraphQL Summit in New York City, October 8 - 10, 2024! 🏙️ Submit your proposal by May 31.
Docs
Launch GraphOS Studio

Entities in Apollo Federation

Resolve types across multiple subgraphs


NOTE

Some details of behavior have changed in Federation 2. For a summary of these changes, see what's new in Federation 2.

In a , an entity is an that can resolve its across multiple . Each subgraph can contribute different fields to the entity and is responsible for resolving only the fields that it contributes.

For example, this Product entity's fields are defined and resolved across two subgraphs:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
Inventory subgraph
type Product @key(fields: "id") {
id: ID!
inStock: Boolean!
}

Entities are a fundamental building block of that enable subgraphs to adhere to the separation of concerns principle.

NOTE

Only can be entities.

Defining an entity

To define an entity within a particular , you do the following:

  1. Apply the @key directive to an object type.
  2. Define the object type's reference resolver.

These steps are described below.

1. Define a @key

In a , you can designate any existing object type as an entity by adding the @key to its definition, like so:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}

The @key directive defines an entity's unique key, which consists of one or more of the type's fields. In the example above, the Product entity's unique key is its id . Every instance of an entity must be uniquely identifiable by its @key fields. This is what enables your to associate field data from different subgraphs with the same entity instance.

In most cases, the @key field(s) for the same entity will be the same across subgraphs. For example, if one subgraph uses id as the @key field for the Product entity, other subgraphs should do the same. However, this isn't strictly required.

If coming from a database context, it can be helpful to think of a @key as an entity's primary key. This term isn't completely accurate for entities since a single entity can have multiple @keys. The field(s) you select for an entity's @key must, however, uniquely identify the entity. In that way, @keys are similar to candidate keys.

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
Inventory subgraph
type Product @key(fields: "id") {
id: ID!
inStock: Boolean!
}

An entity's @key cannot include:

  • that return a union or interface
  • Fields that take s

Though not strictly required, it's best to use non-nullable fields for keys. If you use fields that return null values, may encounter issues resolving the entity.

For more information on advanced key options, like how to define multiple keys or compound keys, see Advanced topics for federation entities.

2. Define a reference resolver

The @key directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its unique key." In order for this to be true, the subgraph needs to define a reference resolver for the entity.

NOTE

This section describes how to create reference in . If you're using another subgraph-compatible library, see its documentation for creating reference resolvers (or the equivalent functionality).

For the Product entity defined above, the reference might look like this:

resolvers.js
// Products subgraph
const resolvers = {
Product: {
__resolveReference(productRepresentation) {
return fetchProductByID(productRepresentation.id);
}
},
// ...other resolvers...
}

Let's break this example down:

  • You declare an entity's reference resolver in your resolver map, as a member of the entity's corresponding object.
  • A reference resolver's name is always __resolveReference.
  • A reference resolver's first parameter is a representation of the entity being resolved.
    • An entity representation is an object that contains the entity's @key fields, plus its __typename field. These values are automatically provided to your subgraph by your router.
  • A reference resolver is responsible for returning all of the entity fields that this subgraph defines.
    • In this example, the hypothetical fetchProductByID function fetches a particular Product's field data based on its id.

NOTE

A particular reference resolver might be called many times to resolve a single . It's crucial that reference resolvers account for "N+1" issues (typically via data loaders). For details, see Handling the N+1 problem.

Every subgraph that contributes at least one unique field to an entity must define a reference resolver for that entity.

To learn more about __resolveReference in Apollo Server, see the API docs.

Contributing entity fields

Any number of different subgraphs can contribute fields to an entity definition. Below, the Products and Inventory subgraphs contribute different fields to the Product entity:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
Inventory subgraph
type Product @key(fields: "id") {
id: ID!
inStock: Boolean!
}

When a subgraph contributes entity fields, no other subgraph knows about those fields—only the router does thanks to the composed .

By default, each subgraph must contribute different fields, with the important exception of @key fields. Otherwise, a error occurs. To override this default, see Resolving another subgraph's field.

As mentioned previously, each subgraph that does contribute fields to an entity must define a reference resolver for that entity.

Referencing an entity without contributing fields

Your subgraphs can use an entity as a field's return type without contributing any fields to that entity. This requires less code than the steps in Defining an entity.

Take a look at this Product entity in the Products subgraph:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}

Now, let's say we want to create a Reviews subgraph that includes the following Review type:

Reviews subgraph
type Review {
product: Product!
score: Int!
}

This is possible. However, this subgraph schema is currently invalid because it doesn't define the Product entity.

To fix this, we can add a stub of the Product entity to the Reviews schema, like so:

Reviews subgraph
type Review {
product: Product!
score: Int!
}
type Product @key(fields: "id", resolvable: false) {
id: ID!
}

As you can see, this stub definition includes only the @key fields of Product (just id in this case). It also includes resolvable: false in the @key directive to indicate that this subgraph doesn't even define a reference resolver for the Product entity.

Example query flow

To help understand how entities are resolved across subgraphs, let's look at an example query executed on an example supergraph.

Let's say we have these two subgraphs that both define the Product entity:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int
}
Reviews subgraph
type Product @key(fields: "id", resolvable: false) {
id: ID!
}
type Review {
score: Int!
description: String!
product: Product!
}
type Query {
latestReviews: [Review!]!
}

NOTE

Notice that the Reviews subgraph references the Product entity without contributing fields.

The Reviews subgraph defines one entry point into our schema: Query.latestReviews. This means that the following query is valid against our router:

query GetReviewsWithProducts {
latestReviews { # Defined in Reviews
score
product {
id
price # ⚠️ NOT defined in Reviews!
}
}
}

Here we have a problem: this query needs to start its execution in the Reviews subgraph (because that's where latestReviews is defined), but that subgraph doesn't know that Product entities have a price field. Remember, the Reviews subgraph only knows about the id field of Product.

Because of this, the router needs to fetch price from the Products subgraph instead. To handle this two-step process, the router generates a query plan.

The query plan

NOTE

are automatically generated and carried out by your router. You don't need to write any code related to them.

A is a blueprint for dividing a single incoming into one or more operations that are each resolvable by a single subgraph. Your router generates a query plan for each unique operation that it receives from clients.

With our example query above, the router knows the following:

  • It must start by the Reviews subgraph, because that's where Query.latestReviews is defined.
  • It must then query the Products subgraph to fetch the price of each Product returned by the Reviews subgraph.

Using this information, the router's query plan starts with this query to the Reviews subgraph:

query {
latestReviews {
score
product {
__typename
id
}
}
}

Notice that this query omits the Product.price field but adds the Product.__typename field. This is because the router needs representations of each returned Product entity for its second query.

NOTE

As described in Define a reference resolver, an entity representation is an object that contains the entity's @key fields (id in this case), plus its __typename field.

This first query returns a list of Review objects, each containing a Product representation. With these representations, the router can execute its second query, this time on the Products subgraph:

query {
_entities(representations: [...]) {
... on Product {
price
}
}
}

This query uses a special entry point that's automatically added to every subgraph schema: Query._entities. This entry point is what provides the router with direct access to any entity's fields.

Each item in the representations list argument above is one of the Product representations that the router obtained from its first query. Here's an example list:

[
{
"__typename": "Product",
"id": "1"
},
{
"__typename": "Product",
"id": "2"
},
//...
]

These representations are passed individually to the Product reference resolver in the Products subgraph. When the reference resolver finishes returning values for each representation, the router receives its response:

[
{
"price": 100
},
{
"price": 200
},
//...
]

Nice! The router can merge this price data with the Product objects returned by its first query. After doing so, the router returns a single, combined result to the client.

Learn more about query plans.

Advanced topics

See Advanced topics on federated entities.

Previous
Sharing types (value types)
Next
Advanced entities
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company