Entities in Apollo Federation

Reference and extend types across subgraphs


In Apollo Federation, an entity is an object type that you define canonically in one subgraph and can then reference and extend in other subgraphs.

Entities are the core building block of a federated graph.

Defining entities

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

GraphQL
products
1type Product @key(fields: "upc") {
2  upc: String!
3  name: String!
4  price: Int
5}

Types besides object types (such as unions and interfaces) cannot be entities.

The @key directive defines the entity's primary key, which consists of one or more of the type's fields. Like primary keys in other systems, an entity's primary key must uniquely identify a particular instance of that entity.

In the example above, the Product entity's primary key is its upc field. The gateway uses an entity's primary key to match data from different subgraphs to the same object instance.

An entity's @key cannot include fields that return a union or interface.

Multiple primary keys

You can define more than one primary key for an entity, when applicable.

In the following example, a Product entity can be uniquely identified by either its upc or its sku:

GraphQL
products
1type Product @key(fields: "upc") @key(fields: "sku") {
2  upc: String!
3  sku: String!
4  price: String
5}

This pattern is helpful when different subgraphs interact with different fields of an entity. For example, a reviews subgraph might refer to products by their UPC, whereas an inventory subgraph might use SKUs.

Compound primary keys

A single primary key can consist of multiple fields, and even nested fields.

The following example shows a primary key that consists of both a user's id and the id of that user's associated organization:

GraphQL
directory
1type User @key(fields: "id organization { id }") {
2  id: ID!
3  organization: Organization!
4}
5
6type Organization {
7  id: ID!
8}

Referencing entities

After you define an entity in one subgraph, other subgraphs can then reference that entity in their schema.

For example, let's say we have a products subgraph that defines the following Product entity:

GraphQL
products
1type Product @key(fields: "upc") {
2  upc: String!
3  name: String!
4  price: Int
5}

A reviews subgraph can then add a field of type Product to its Review type, like so:

GraphQL
reviews
1type Review {
2  score: Int!
3  product: Product!
4}
5
6# This is a required "stub" of the Product entity (see below)
7extend type Product @key(fields: "upc") {
8  upc: String! @external
9}

To reference an entity that originates in another subgraph, the reviews subgraph needs to define a stub of that entity to make its own schema valid. The stub includes just enough information for the subgraph to know how to uniquely identify a particular Product:

  • The extend keyword indicates that Product is an entity that's defined in another subgraph.

  • The @key directive indicates that Product uses the upc field as its primary key. This value must match the value of exactly one @key defined in the entity's originating subgraph (even if the entity defines multiple primary keys).

  • The upc field must be present because it's part of the specified @key. It also requires the @external directive to indicate that it originates in another subgraph.

This explicit syntax has several benefits:

  • It's standard GraphQL grammar.

  • It enables you to run the reviews subgraph standalone with a valid schema, including a Product type with a single upc field.

  • It provides strong typing information that lets you catch mistakes at schema composition time.

Resolving entities

Let's say our reviews subgraph from Referencing entities defines the following Query type:

GraphQL
reviews
1type Query {
2  latestReviews: [Review!]
3}

That means the following query is valid against our federated graph:

GraphQL
1query GetReviewsAndProducts {
2  latestReviews {
3    score
4    product {
5      upc
6      price # Not defined in reviews!
7    }
8  }
9}

Now we have a problem: this query starts its execution in the reviews subgraph (where latestReviews is defined), but that subgraph doesn't know that Product entities have a price field! Remember, the reviews subgraph only knows about its stub fields of Product.

Because of this, the gateway needs to fetch price from the products subgraph instead. But how does the gateway know which products it needs to fetch the prices for?

To solve this, we add a resolver to each subgraph:

Entity representations

In our example, the reviews subgraph needs to define a resolver for its stub version of the Product entity. The reviews subgraph doesn't know much about Products, but fortunately, it doesn't need to. All it needs to do is return data for the fields it does know about, like so:

JavaScript
resolvers.js
1// Reviews subgraph
2const resolvers = {
3  Review: {
4    product(review) {
5      return {
6        __typename: "Product",
7         upc: review.upc
8      };
9    }
10  },
11  // ...
12}

This resolver's return value is a representation of a Product entity (because it represents an entity from another subgraph). A representation always consists of:

  • A __typename field

  • Values for the entity's primary key fields (upc in this example)

Because an entity can be uniquely identified by its primary key fields, this is all the information the gateway needs to fetch additional fields for a Product object.

Reference resolvers

As a reminder, here's the example query we're executing across our subgraphs:

GraphQL
1query GetReviewsAndProducts {
2  latestReviews {
3    score
4    product {
5      upc
6      price # Not defined in reviews!
7    }
8  }
9}

The gateway knows it can't fetch Product.price from the reviews subgraph, so first it executes the following query on reviews:

GraphQL
1query {
2  latestReviews {
3    score
4    product { # List of Product representations
5      __typename
6      upc
7    }
8  }
9}

Notice that this query omits price but adds __typename, even though it wasn't in the original query string! This is because the gateway knows it needs all of the fields in each Product's representation, including __typename.

With these representations available, the gateway can now execute a second query on the products subgraph to fetch each product's price. To support this special query, the products subgraph needs to define a reference resolver for the Product entity:

JavaScript
resolvers.js
1// Products subgraph
2const resolvers = {
3  Product: {
4    __resolveReference(reference) {
5      return fetchProductByUPC(reference.upc);
6    }
7  },
8  // ...
9}

In the example above, fetchProductByUPC is a hypothetical function that fetches a Product's full details from a data store based on its upc.

A reference resolver (always called __resolveReference) provides the gateway direct access to a particular entity's fields, without needing to use a custom query to reach that entity. To use a reference resolver, the gateway must provide a valid entity representation, which is why we created the resolver in the reviews subgraph first!

To learn more about __resolveReference, see the API docs.

After fetching the price field from products via a reference resolver, the gateway can intelligently merge the data it obtained from its two queries into a single result and return that result to the querying client.

Extending entities

A subgraph can add fields to an entity that's defined in another subgraph. This is called extending the entity.

When a subgraph extends an entity, the entity's originating subgraph is not aware of the added fields. Only the extending subgraph (along with the gateway) knows about these fields.

Each field of an entity should be defined in exactly one subgraph. Otherwise, a schema composition error will occur.

Example #1

Let's say we want to add a reviews field to the Product entity. This field will return a list of reviews for the product. The Product entity originates in the products subgraph, but it makes more sense for the reviews subgraph to resolve this particular field.

To handle this case, we can extend the Product entity in the reviews subgraph, like so:

GraphQL
reviews
1extend type Product @key(fields: "upc") {
2  upc: String! @external
3  reviews: [Review]
4}

This definition is nearly identical to the stub we defined for the Product type in Referencing entities. All we've added is the reviews field. We don't include an @external directive, because this field does originate in the reviews subgraph.

Whenever a subgraph extends an entity with a new field, it's also responsible for resolving that field. The gateway is automatically aware of this responsibility. In our example:

  1. The gateway first fetches the upc field for each Product from the products subgraph.

  2. The gateway then passes those upc values to the reviews subgraph, where you can access them on the object passed to your Product.reviews resolver:

JavaScript
1{
2  Product: {
3    reviews(product) {
4      return fetchReviewsForProduct(product.upc);
5    }
6  }
7}

Example #2

Let's say we want to be able to query for the inStock status of a product. That information lives in an inventory subgraph, so we'll add the type extension there:

GraphQL
inventory
1extend type Product @key(fields: "upc") {
2  upc: ID! @external
3  inStock: Boolean
4}
JavaScript
1{
2  Product: {
3    inStock(product): {
4      return fetchInStockStatusForProduct(product.upc);
5    }
6  }
7}

Similar to the previous example, the gateway fetches the required upc field from the products subgraph and passes it to the inventory subgraph, even if the query didn't ask for the upc:

GraphQL
1# This query fetches upc from the products subgraph even though
2# it isn't a requested field. Otherwise, the inventory subgraph
3# can't know which products to return the inStock status for.
4query GetTopProductAvailability {
5  topProducts {
6    name
7    inStock
8  }
9}

The Query and Mutation types

In Apollo Federation, the Query and Mutation base types originate in the graph composition itself and all of your subgraphs are automatically treated as extending these types to add the operations they support without explicitly adding the extends keyword.

For example, the products subgraph might extend the root Query type to add a topProducts query, like so:

GraphQL
products
1type Query {
2  topProducts(first: Int = 5): [Product]
3}

Migrating entities and fields (advanced)

As your federated graph grows, you might decide that you want an entity (or a particular field of an entity) to originate in a different subgraph. This section describes how to perform these migrations.

Entity migration

Let's say our Payments subgraph defines a Bill entity:

GraphQL
1# Payments subgraph
2type Bill @key(fields: "id") {
3  id: ID!
4  amount: Int!
5}
6
7type Payment {
8  # ...
9}

Then, we add a dedicated Billing subgraph to our federated graph. It now makes sense for the Bill entity to originate in the Billing subgraph instead. When we're done migrating, we want our deployed subgraph schemas to look like this:

GraphQL
1# Payments subgraph
2type Payment {
3  # ...
4}
GraphQL
1# Billing subgraph
2type Bill @key(fields: "id") {
3  id: ID!
4  amount: Int!
5}

The exact steps depend on how you perform schema composition:

With Rover CLI composition
  1. In the Billing subgraph's schema, define the Bill entity just as it's defined in the Payments subgraph (do not extend it):
    GraphQL
    1# Payments subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
    6
    7type Payment {
    8  # ...
    9}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
    • Note that if you perform composition at this point, it produces an error because the Bill entity can't originate in more than one subgraph. That's okay! We aren't running composition yet, and we'll resolve this error in a couple of steps.
  2. In the Billing subgraph, define resolvers for each field of Bill that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph.
  3. Deploy the updated Billing subgraph to your environment.
    • Again, this technically deploys a composition error. However, your gateway isn't aware of this! It's still using the original supergraph schema, which indicates that Bill originates only in the Payments subgraph.
  4. In the Payments subgraph, remove the Bill entity and its associated resolvers (do not deploy this change yet):
    GraphQL
    1# Payments subgraph
    2type Payment {
    3  # ...
    4}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
    • This takes care of the composition error in your local environment.
  5. Compose an updated supergraph schema with your usual configuration using rover supergraph compose.
    • This updated supergraph schema indicates that Bill originates in the Billing subgraph.
  6. Assuming CI completes successfully, deploy an updated version of your gateway with the new supergraph schema.
    • When this deployment completes, the gateway begins resolving Bill fields in the Billing subgraph instead of the Payments subgraph.
  7. Deploy an updated version of your Payments subgraph without the Bill entity.
    • At this point it's safe to remove this definition, because the gateway is never resolving Bill with the Payments subgraph.
We're done! Bill now originates in a new subgraph, and it was resolvable during each step of the migration process.
With managed federation
  1. In the Billing subgraph's schema, define the Bill entity just as it's defined in the Payments subgraph (do not extend it):
    GraphQL
    1# Payments subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
    6
    7type Payment {
    8  # ...
    9}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
  2. Publish the updated Billing subgraph schema to Apollo.
    • Note that you're publishing a composition error, because the Bill entity now originates in more than one subgraph. This creates a failed launch in Apollo Studio. That's okay! Apollo Uplink continues to serve the most recent valid supergraph schema to your gateway.
  3. In the Billing subgraph, define resolvers for each field of Bill that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph.
  4. Deploy the updated Billing subgraph to your environment.
    • This change is invisible to your gateway, which is not yet aware that the Billing subgraph defines the Bill entity.
  5. In the Payments subgraph, remove the Bill entity and its associated resolvers:
    GraphQL
    1# Payments subgraph
    2type Payment {
    3  # ...
    4}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
  6. Deploy the updated Payments subgraph to your environment.
    • This resolves the composition error, because Bill now originates in a single subgraph. Apollo composes an updated supergraph schema, which your gateway automatically obtains when it polls the Apollo Uplink.
We're done! Bill now originates in a new subgraph, and it was resolvable during each step of the migration process.
With `IntrospectAndCompose`
⚠️ We strongly recommend against using IntrospectAndCompose in production. For details, see Limitations of IntrospectAndCompose.
When you provide IntrospectAndCompose to ApolloGateway, it performs composition itself on startup after fetching all of your subgraph schemas. If this runtime composition fails, the gateway fails to start up, resulting in downtime.To minimize downtime for your graph, you need to make sure all of your subgraph schemas successfully compose whenever your gateway starts up. When migrating an entity, this requires a coordinated deployment of your modified subgraphs and a restart of the gateway itself.
  1. In the Billing subgraph's schema, define the Bill entity just as it's defined in the Payments subgraph (do not extend it):
    GraphQL
    1# Payments subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
    6
    7type Payment {
    8  # ...
    9}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
  2. In the Billing subgraph, define resolvers for each field of Bill that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph.
  3. In the Payments subgraph's schema, remove the Bill entity and its associated resolvers:
    GraphQL
    1# Payments subgraph
    2type Payment {
    3  # ...
    4}
    GraphQL
    1# Billing subgraph
    2type Bill @key(fields: "id") {
    3  id: ID!
    4  amount: Int!
    5}
  4. Bring down all instances of your gateway in your deployed environment. This downtime prevents inconsistent behavior during a rolling deploy of your subgraphs.
  5. Deploy the updated Payments and Billing subgraphs to your environment. When these deployments complete, bring your gateway instances back up and confirm that they start up successfully.

Field migration

The steps for migrating an individual field are nearly identical in form to the steps for migrating an entire entity.

Let's say our Products subgraph defines a Product entity, which includes the boolean field inStock:

GraphQL
1# Products subgraph
2type Product @key(fields: "id") {
3  id: ID!
4  inStock: Boolean!
5}

Then, we add an Inventory subgraph to our federated graph. It now makes sense for the inStock field to originate in the Inventory subgraph instead, like this:

GraphQL
1# Products subgraph
2type Product @key(fields: "id") {
3  id: ID!
4}
GraphQL
1# Inventory subgraph
2extend type Product @key(fields: "id") {
3  id: ID! @external
4  inStock: Boolean!
5}

We can perform this migration with the following steps (additional commentary on each step is provided in Entity migration):

  1. In the Inventory subgraph's schema, extend the Product entity to add the inStock field:

    GraphQL
    1# Products subgraph
    2type Product @key(fields: "id") {
    3  id: ID!
    4  inStock: Boolean!
    5}
    GraphQL
    1# Inventory subgraph
    2extend type Product @key(fields: "id") {
    3  id: ID! @external
    4  inStock: Boolean!
    5}
    • If you're using managed federation, register this schema change with Apollo.

  2. In the Inventory subgraph, add a resolver for the inStock field. This subgraph should resolve the field with the exact same logic as the resolver in the Products subgraph.

  3. Deploy the updated Inventory subgraph to your environment.

  4. In the Products subgraph's schema, remove the inStock field and its associated resolver:

    GraphQL
    1# Products subgraph
    2type Product @key(fields: "id") {
    3  id: ID!
    4}
    GraphQL
    1# Inventory subgraph
    2extend type Product @key(fields: "id") {
    3  id: ID! @external
    4  inStock: Boolean!
    5}
    • If you're using managed federation, register this schema change with Studio.

  5. If you're using Rover composition, compose a new supergraph schema. Deploy a new version of your gateway that uses the updated schema.

    • Skip this step if you're using managed federation.

  6. Deploy the updated Products subgraph to your environment.

Extending an entity with computed fields (advanced)

When you extend an entity, you can define fields that depend on fields in the entity's originating subgraph. For example, a shipping subgraph might extend the Product entity with a shippingEstimate field, which is calculated based on the product's size and weight:

GraphQL
shipping
1extend type Product @key(fields: "sku") {
2  sku: ID! @external
3  size: Int @external
4  weight: Int @external
5  shippingEstimate: String @requires(fields: "size weight")
6}

As shown, you use the @requires directive to indicate which fields (and subfields) from the entity's originating subgraph are required.

You cannot require fields that are defined in a subgraph besides the entity's originating subgraph.

In the above example, if a client requests a product's shippingEstimate, the gateway will first obtain the product's size and weight from the products subgraph, then pass those values to the shipping subgraph. This enables you to access those values directly from your resolver:

JavaScript
1{
2  Product: {
3    shippingEstimate(product) {
4      return computeShippingEstimate(product.sku, product.size, product.weight);
5    }
6  }
7}

Using @requires with object subfields

If a computed field @requires a field that returns an object type, you also specify which subfields of that object are required. You list those subfields with the following syntax:

GraphQL
shipping
1extend type Product @key(fields: "sku") {
2  sku: ID! @external
3  dimensions: ProductDimensions @external
4  shippingEstimate: String @requires(fields: "dimensions { size weight }")
5}

In this modification of the previous example, size and weight are now subfields of a ProductDimensions object. Note that the ProductDimensions object must be defined in both the entity's extending subgraph and its originating subgraph, either as an entity or as a value type.

Resolving another subgraph's field (advanced)

Sometimes, multiple subgraphs are capable of resolving a particular field for an entity, because all of those subgraphs have access to a particular data store. For example, an inventory subgraph and a products subgraph might both have access to the database that stores all product-related data.

When you extend an entity in this case, you can specify that the extending subgraph @provides the field, like so:

GraphQL
inventory
1type InStockCount {
2  product: Product! @provides(fields: "name price")
3  quantity: Int!
4}
5
6extend type Product @key(fields: "sku") {
7  sku: String! @external
8  name: String @external
9  price: Int @external
10}

This is a completely optional optimization. When the gateway plans a query's execution, it looks at which fields are available from each subgraph. It can then attempt to optimize performance by executing the query across the fewest subgraphs needed to access all required fields.

Keep the following in mind when using the @provides directive:

  • Each subgraph that @provides a field must also define a resolver for that field. That resolver's behavior must match the behavior of the resolver in the field's originating subgraph.

  • When an entity's field can be fetched from multiple subgraphs, there is no guarantee as to which subgraph will resolve that field for a particular query.

  • If a subgraph @provides a field, it must still list that field as @external, because the field originates in another subgraph.

Feedback

Edit on GitHub

Forums