Docs
Launch GraphOS Studio

Advanced topics on federated entities


This article describes complex behaviors of federated entities beyond those covered in entity basics.

Using advanced @keys

Depending on your entities' s and usage, you may need to use more advanced @keys. For example, you may need to define a compound @key if multiple s are required to uniquely identify an entity. If different s interact with different fields an entity, you may need to define multipleand sometimes differing@keys for the entity.

Compound @keys

A single @key can consist of multiple s, the combination of which uniquely identifies an entity. This is called a compound or composite key. In the following example, the combination of both username and domain s is required to uniquely identify the User entity:

Users subgraph
type User @key(fields: "username domain") {
username: String!
domain: String!
}

Nested fields in compound @keys

Compound keys can also include nested s. In the following example, the User entity's @key consists of both a user's id and the id of that user's associated Organization:

Users subgraph
type User @key(fields: "id organization { id }") {
id: ID!
organization: Organization!
}
type Organization {
id: ID!
}

Multiple @keys

When different s interact with different s of an entity, you may need to define multiple @keys for the entity. For example, a Reviews might refer to products by their ID, whereas an Inventory subgraph might use SKUs.

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

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

Note: If you include multiple sets of @key s, the planner uses the most efficient set for entity resolution. For example, suppose you allow a type to be identified by @key(fields: "id") or @key(fields: "id sku"):

type Product @key(fields: "id") @key(fields: "id sku") {
# ...
}

That means either id or (id and sku) is enough to uniquely identify the entity. Since id alone is enough, the planner will use only that to resolve the entity, and @key(fields: "id sku") is effectively ignored.

Referencing entities with multiple keys

A that references an entity without contributing any fields can use any @key s in its stub definition. For example, if the Products defines the Product entity like this:

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

Then, a Reviews can use either id or sku in the stub definition:

Reviews subgraph
# Either:
type Product @key(fields: "id", resolvable: false) {
id: ID!
}
# Or:
type Product @key(fields: "sku", resolvable: false) {
sku: String!
}

When resolving a reference for an entity with multiple keys, you can determine how to resolve it based on which key is present. For example, if you're using @apollo/subgraph, it could look like this:

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

Differing @keys across subgraphs

Although an entity commonly uses the exact same @key (s) across s, you can alternatively use different @keys with different s. For example, you can define a Product entity shared between s, one with sku and upc as its @keys, and the other with only upc as the @key :

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

To merge entities between s, the entity must have at least one shared between subgraphs. For example, s can't merge the Product entity defined in the following s because they don't share any s specified in the @key selection set:

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

Operations with differing @keys

Differing keys across s affect which of the entity's s can be resolved from each subgraph. Requests can resolve fields if there is a traversable path from the root query to the fields.

Take these s as an example:

Products subgraph
type Product @key(fields: "sku") {
sku: ID!
upc: String!
name: String!
price: Int
}
type Query {
product(sku: ID!): Product
products: [Product!]!
}
Inventory subgraph
type Product @key(fields: "upc") {
upc: String!
inStock: Boolean!
}

The queries defined in the Products can always resolve all product s because the product entity can be joined via the upc present in both schemas.

On the other hand, queries added to the Inventory can't resolve s from the Products subgraph:

Products subgraph
type Product @key(fields: "sku") {
sku: ID!
upc: String!
name: String!
price: Int
}
Inventory subgraph
type Product @key(fields: "upc") {
upc: String!
inStock: Boolean!
}
type Query {
productsInStock: [Product!]!
}

The productsInStock can't resolve s from the Products since the Products subgraph's Product type definition doesn't include upc as a key , and sku isn't present in the Inventory .

If the Products includes @key(fields: "upc"), all queries from the Inventory can resolve all product s:

Products subgraph
type Product @key(fields: "sku") @key(fields: "upc") {
sku: ID!
upc: String!
name: String!
price: Int
}
Inventory subgraph
type Product @key(fields: "upc") {
upc: String!
inStock: Boolean!
}
type Query {
productsInStock: [Product!]!
}

Migrating entities and fields

As your grows, you might want to move parts of an entity to a different . This section describes how to perform these migrations safely.

Incremental migration with @override

Let's say our Payments defines a Bill entity:

Payments subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
payment: Payment
}
type Payment {
# ...
}

Then, we add a dedicated Billing to our . It now makes sense to move billing functionality there. When we're done migrating, we want our deployed s to look like this:

Payments subgraph
type Bill @key(fields: "id") {
id: ID!
payment: Payment
}
type Payment {
# ...
}
Billing subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}

The @override directive enables us to perform this migration incrementally with no downtime.

First, we deploy a new version of the Billing that defines and resolves the Bill s we want to move:

Payments subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
payment: Payment
}
type Payment {
# ...
}
Billing subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int! @override(from: "Payments")
}

The @override says, "Resolve this in this instead of in the Payments ."

In any where you use @override, make sure to include it in your schema's @link imports (code-first libraries usually do this for you):

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

Next, we update our 's to include the updated Billing . If you're using , you do this by publishing the Billing subgraph's schema to with rover subgraph publish.

When the receives its updated , it immediately starts resolving the Bill.amount from the Billing while continuing to resolve Bill.payment from the Payments .

We can migrate as many entity s as we want in a single change. To do so, we apply @override to every entity we want to move. We can even migrate entire entities this way!

Now that Bill.amount is resolved in the Billing , we can safely remove that (and its ) from the Payments :

Payments subgraph
type Bill @key(fields: "id") {
id: ID!
payment: Payment
}
type Payment {
# ...
}
Billing subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int! @override(from: "Payments")
}

After making this change, we deploy our updated Payments and again update our 's .

Because the is already ignoring Bill.amount in the Payments thanks to @override, we can safely publish our updated schema and deploy the in any order!

Finally, we can remove the @override from the Billing , because it no longer has any effect:

Payments subgraph
type Bill @key(fields: "id") {
id: ID!
payment: Payment
}
type Payment {
# ...
}
Billing subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}

After we deploy the Billing and publish this final schema change, we're done! We've migrated Bill.amount to the Billing with zero downtime.

Optimizing for fewer deploys with manual composition

⚠️ This method requires careful coordination between subgraph and router updates. Without strict control over the order of deployments and schema updates, you might cause an outage. For most use cases, we recommend using the @override method above.

Using @override to migrate entity s enables us to migrate fields incrementally with zero downtime. However, doing so requires three separate schema publishes. If you're using manual composition, each schema change requires redeploying your . With careful coordination, we can perform the same migration with only a single redeploy.

  1. In the Billing , define the Bill entity, along with its corresponding s. These new resolvers should behave identically to the Payment subgraph resolvers they're replacing.

    Payments subgraph
    type Bill @key(fields: "id") {
    id: ID!
    amount: Int!
    payment: Payment
    }
    type Payment {
    # ...
    }
    Billing subgraph
    type Bill @key(fields: "id") {
    id: ID!
    amount: Int!
    }
  2. Deploy the updated Billing to your environment, but do not publish the updated schema yet.

    • At this point, the Billing can successfully resolve Bill objects, but the doesn't know this yet because its hasn't been updated. Publishing the schema would cause a error.
  3. In the Payments , remove the migrated s from the Bill entity and their associated s (do not deploy this change yet):

    Payments subgraph
    type Bill @key(fields: "id") {
    id: ID!
    payment: Payment
    }
    type Payment {
    # ...
    }
    Billing subgraph
    type Bill @key(fields: "id") {
    id: ID!
    amount: Int!
    }
  4. Compose an updated with your usual configuration using rover supergraph compose.

    • This updated indicates that the Billing resolves Bill.amount, and the Payments doesn't.
  5. Assuming CI completes successfully, deploy an updated version of your router with the new .

    • When this deployment completes, the begins resolving Bill s in the Billing instead of the Payments .

    ⚠️ While your new router instances are deploying, you will probably have active router instances resolving the Bill.amount in two different ways (with older instances still resolving it from Payments). It's important that the two s resolve the field in exactly the same way, or your clients might see inconsistent data during this rollover.

  6. Deploy the updated version of your Payments without the migrated .

    • At this point it's safe to remove this definition, because your instances are using the Billing exclusively.

We're done! The migrated s have been moved to a new , and we only redeployed our once.

Contributing computed entity fields

You can define s of an entity that are computed based on the values of other entity s that are resolved by a different .

For example, this Shipping adds a shippingEstimate to the Product entity. This is calculated based on the product's size and weight, which are defined in the Products :

Shipping subgraph
type Product @key(fields: "id") {
id: ID!
size: Int @external
weight: Int @external
shippingEstimate: String @requires(fields: "size weight")
}

As shown, you use the @requires to indicate which s (and subfields) from other s are required. You also need to define the required s and apply the @external to them. This directive tells the , "This knows that these s exist, but it can't resolve them itself."

In the above example, if a requests a product's shippingEstimate, the does the following, in order:

  1. It queries the Products for the product's size and weight.
  2. It queries the Shipping for the product's shippingEstimate. The size and weight are included in the Product object passed to the for shippingEstimate:
{
Product: {
shippingEstimate(product) {
return computeShippingEstimate(product.id, product.size, product.weight);
}
}
}

Using @requires with object subfields

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

Shipping subgraph
type Product @key(fields: "id") {
id: ID!
dimensions: ProductDimensions @external
shippingEstimate: String @requires(fields: "dimensions { size weight }")
}

In this modification of the previous example, size and weight are now subs of a ProductDimensions object. Note that the ProductDimensions type must be defined in both the Products and Shipping s for this to be valid.

Using @requires with fields that take arguments

This functionality was introduced in Federation v2.1.2.

The @requires can include s that take s, like so:

Shipping subgraph
type Product @key(fields: "id") {
id: ID!
weight(units: String): Int @external
shippingEstimate: String @requires(fields: "weight(units:\"KILOGRAMS\")")
}
  • The provides the specified values in its to whichever defines the required .
  • Each specified value is static (i.e., the always provides the same value).
  • You can omit values for nullable s. You must provide values for non-nullable s.
  • If you define your in an file (instead of programmatically), you must escape quotes for string and enum values with backslashes (as shown above).

Resolving another subgraph's field

By default, exactly one is responsible for resolving each in your (with important exceptions, like entity @key s). But sometimes, multiple s are able to resolve a particular 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.

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

You achieve this with one of the following s:

Which you use depends on the following logic:

Always
Only certain query paths
Can my subgraph always resolve this field,
or only from certain query paths?
@shareable
@provides

If you aren't sure whether your can always resolve a , see Using @provides for an example of a that can't.

Ensure resolver consistency

If multiple s can resolve a , make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results to clients depending on which resolves the .

This is especially important to keep in mind when making changes to an existing . If you don't make the resolver changes to each simultaneously, clients might observe inconsistent results.

Common inconsistent behaviors to look out for include:

  • Returning a different default value
  • Throwing different errors in the same scenario

Using @shareable

⚠️ Before using @shareable, see Ensure resolver consistency.

The @shareable indicates that a particular can be resolved by more than one :

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

In this example, both the Products and Inventory s can resolve Product.name. This means that a that includes Product.name might be resolvable by fetching from fewer total s.

If a is marked @shareable in any , it must be marked @shareable or @external in every that defines it. Otherwise, fails.

Using @provides

⚠️ Before using @provides, see Ensure resolver consistency.

The @provides indicates that a particular can be resolved by a at a particular query path. Let's look at an example.

Here, our Products defines a Product.name and marks it @shareable (this means other s are allowed to resolve it):

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

Meanwhile, our Inventory can also resolve a product's name, but only when that product is part of an InStockCount:

Inventory subgraph
type InStockCount {
product: Product! @provides(fields: "name")
quantity: Int!
}
type Product @key(fields: "id") {
id: ID!
name: String! @external
inStock: Boolean!
}

Here we're using two s in combination: @provides and @external.

  • The @provides tells the , "This can resolve the name of any Product object returned by InStockCount.product."
  • The @external tells the , "This can't resolve the name of a Product object, except wherever indicated by @provides."

Rules for using @provides

  • If a @provides a that it can't always resolve, the must mark that as @external and must not mark it as @shareable.
    • Remember, a @shareable can always be resolved by a particular , which removes the need for @provides.
  • To include a in a @provides , that must be marked as @shareable or @external in every that defines it.

Violating any of these rules causes to fail.

Handling the N+1 problem

Most implementations use reference resolvers (sometimes known as entity s) to handle the Query._entities ergonomically. A reference is passed a single key and returns the entity object that corresponds to that key.

Although this pattern is straightforward, it can diminish performance when a client requests s from many entities. To illustrate this, let's revisit an earlier example:

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

As mentioned in The query plan, the executes two queries on its s to resolve the above :

  1. It queries the Reviews to fetch all s except Product.price.
  2. It queries the Products to fetch the price of each Product entity.

In the Products , the reference for Product doesn't take a list of keys, but rather a single key. Therefore, the library calls the reference once for each key:

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

A basic implementation of the fetchProductByID function might make a database call each time it's called. If we need to resolve Product.price for N different products, this results in N database calls. These calls are made in addition to the call made by the Reviews to fetch the initial list of reviews (and the id of each product). This is where the "N+1" problem gets its name. If not prevented, this problem can cause performance problems or even enable denial-of-service attacks.

This problem is not limited to reference s! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using the dataloader pattern. Nearly every library provides a dataloader implementation, and you should use it in every resolver. This is true even for s that aren't for entities and that don't return a list. These s can still cause N+1 issues via batched requests.

Previous
Entities (basics)
Next
Entity interfaces
Edit on GitHubEditForumsDiscord