Advanced topics on federated entities
This article describes complex behaviors of federated entities beyond those covered in entity basics.
Advanced @key
s
A single entity can have multiple @key
s. Additionally, a @key
can include multiple fields, and even arbitrarily nested fields.
Multiple @key
s
You can define more than one @key
for an entity, when applicable.
In this example, a Product
entity can be uniquely identified by either its id
or its sku
:
type Product @key(fields: "id") @key(fields: "sku") {id: ID!sku: String!name: String!price: Int}
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 ID, whereas an inventory
subgraph might use SKUs.
A subgraph that references an entity without contributing any fields can include the fields of any @key
in its stub definition:
# Either:type Product @key(fields: "id", resolvable: false) {id: ID!}# Or:type Product @key(fields: "sku", resolvable: false) {sku: String!}
Compound @key
s
A single @key
can consist of multiple fields, and even nested fields.
In this example, the User
entity's primary key consists of both a user's id
and the id
of that user's associated Organization
:
type User @key(fields: "id organization { id }") {id: ID!organization: Organization!}type Organization {id: ID!}
Migrating entities and fields
As your federated graph grows, you might decide that you want to move an entity's definition (or a particular field of an entity) to a different subgraph. This section describes how to perform these migrations.
Entity migration
Let's say our Payments subgraph defines a Bill
entity:
type Bill @key(fields: "id") {id: ID!amount: Int!}type Payment {# ...}
Then, we add a dedicated Billing subgraph to our federated graph. It now makes sense to define the Bill
entity there instead. When we're done migrating, we want our deployed subgraph schemas to look like this:
type Payment {# ...}
type Bill @key(fields: "id") {id: ID!amount: Int!}
The steps for this migration depend on how you perform schema composition:
Field migration
The steps for migrating an individual field are nearly identical in form to the steps for migrating an entire entity.
If you're using managed federation, follow the steps in the With managed federation section above, but apply the @override
directive to the individual field you're migrating instead of all fields.
If you're using local composition with Rover, see below for a field migration example.
Local composition example
Let's say our Products subgraph defines a Product
entity, which includes the boolean field inStock
:
type Product @key(fields: "id") {id: ID!inStock: Boolean!}
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:
type Product @key(fields: "id") {id: ID!}
type Product @key(fields: "id") {id: ID!inStock: Boolean!}
We can perform this migration with the following steps (additional commentary on each step is provided in Entity migration):
In the Inventory subgraph's schema, define the
Product
entity and include theinStock
field:Products subgraphtype Product @key(fields: "id") {id: ID!inStock: Boolean!}Inventory subgraphtype Product @key(fields: "id") {id: ID!inStock: Boolean!}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.Deploy the updated Inventory subgraph to your environment.
In the Products subgraph's schema, remove the
inStock
field and its associated resolver:Products subgraphtype Product @key(fields: "id") {id: ID!}Inventory subgraphtype Product @key(fields: "id") {id: ID!inStock: Boolean!}Compose a new supergraph schema. Deploy a new version of your gateway that uses the updated schema.
Deploy the updated Products subgraph to your environment.
Contributing computed entity fields
You can define fields of an entity that are computed based on the values of other entity fields that are resolved by a different subgraph.
For example, this Shipping subgraph adds a shippingEstimate
field to the Product
entity. This field is calculated based on the product's size
and weight
, which are defined in the Products subgraph:
type Product @key(fields: "id") {id: ID!size: Int @externalweight: Int @externalshippingEstimate: String @requires(fields: "size weight")}
As shown, you use the @requires
directive to indicate which fields (and subfields) from other subgraphs are required. You also need to define the required fields and apply the @external
directive to them. This directive tells the gateway, "This subgraph knows that these fields exist, but it can't resolve them itself."
In the above example, if a query requests a product's shippingEstimate
, the gateway does the following, in order:
- It queries the Products subgraph for the product's
size
andweight
. - It queries the Shipping subgraph for the product's
shippingEstimate
. Thesize
andweight
are included in theProduct
object passed to the resolver forshippingEstimate
:
{Product: {shippingEstimate(product) {return computeShippingEstimate(product.id, product.size, product.weight);}}}
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:
type Product @key(fields: "id") {id: ID!dimensions: ProductDimensions @externalshippingEstimate: String @requires(fields: "dimensions { size weight }")}
In this modification of the previous example, size
and weight
are now subfields of a ProductDimensions
object. Note that the ProductDimensions
type must be defined in both the Products and Shipping subgraphs for this to be valid.
Resolving another subgraph's field
By default, exactly one subgraph is responsible for resolving each field in your supergraph schema (with some exceptions, like entity @key
fields). But sometimes, multiple subgraphs are able to resolve a particular entity field, 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 subgraphs to resolve a particular entity field. 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.
You achieve this with one of the following directives:
Which directive you use depends on the following logic:
If you aren't sure whether your subgraph can always resolve a field, see Using @provides
for an example of a subgraph that can't.
Ensure resolver consistency
If multiple subgraphs can resolve a field, make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results to clients depending on which subgraph resolves the field.
This is especially important to keep in mind when making changes to an existing resolver. If you don't make the resolver changes to each subgraph simultaneously, clients might observe inconsistent results.
Common inconsistent resolver 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
directive indicates that a particular field can be resolved by more than one subgraph:
type Product @key(fields: "id") {id: ID!name: String! @shareableprice: Int}
type Product @key(fields: "id") {id: ID!name: String! @shareableinStock: Boolean!}
In this example, both the Products and Inventory subgraphs can resolve Product.name
. This means that a query that includes Product.name
might be resolvable by fetching from fewer total subgraphs.
If a field is marked @shareable
in any subgraph, it must be marked @shareable
or @external
in every subgraph that defines it. Otherwise, composition fails.
Using @provides
⚠️ Before using @provides
, see Ensure resolver consistency.
The @provides
directive indicates that a particular field can be resolved by a subgraph at a particular query path. Let's look at an example.
Here, our Products subgraph defines a Product.name
field and marks it @shareable
(this means other subgraphs are allowed to resolve it):
type Product @key(fields: "id") {id: ID!name: String! @shareableprice: Int}
Meanwhile, our Inventory subgraph can also resolve a product's name, but only when that product is part of an InStockCount
:
type InStockCount {product: Product! @provides(fields: "name")quantity: Int!}type Product @key(fields: "id") {id: ID!name: String! @externalinStock: Boolean!}
Here we're using two directives in combination: @provides
and @external
.
- The
@provides
directive tells the gateway, "This subgraph can resolve thename
of anyProduct
object returned byInStockCount.product
." - The
@external
directive tells the gateway, "This subgraph can't resolve thename
of aProduct
object, except wherever indicated by@provides
."
Rules for using @provides
- If a subgraph
@provides
a field that it can't always resolve, the subgraph must mark that field as@external
and must not mark it as@shareable
.- Remember, a
@shareable
field can always be resolved by a particular subgraph, which removes the need for@provides
.
- Remember, a
- To include a field in a
@provides
directive, that field must be marked as@shareable
or@external
in every subgraph that defines it.
Violating any of these rules causes composition to fail.