Federation directives

Subgraphs are regular GraphQL APIs. Composition works without intervention if the subgraphs are independent from another and do not share types and fields. But composition can also act as much more than schema stitching: federation directives allow each subgraph to declaratively specify what it can resolve and how it fits with other subgraphs in the federated graph, so the router can expose a consistent whole with connections that span across subgraphs, with minimal coordination between the teams owning each subgraph. Read on to understand what directives exist and what they do, or read the Use cases.

directive @key( fields: FieldSet! resolvable: Boolean = true ) repeatable on OBJECT | INTERFACE

The @key directive is how entities are defined. Entities are types with a key that exist in multiple subgraphs. This is the main mechanism that connects subgraphs together in Federation. It can be thought of as a primary key.

When you make a type an entity with the @key directive, your Federation compatible GraphQL framework of choice will also require that you define an entity resolver for that type. This is simply a resolver that will be used in the Federation-specific Query._entities field to fetch

Arguments:

  • fields: a string containing GraphQL selection set for the fields included in the key. The selection can be nested (e.g. @key(fields: "a { b } c d")) but it cannot include field arguments (e.g. @key(fields: "id(type: UUID) { bytes }") is not valid).
  • resolvable: when false, indicates that the subgraph references an entity — often by returning its key — but cannot resolve it through Query._entities. In other words, there is no entity resolver for that entity in this subgraph. This is useful when a subgraph has the key for an entity — for example an author_id on a blog post — but does not contribute fields to that entity.

Let us use a fictitious e-commerce website to showcase what you can do with entities. First we define an inventory subgraph:

type Product @key(fields: "id") @key(fields: "sku") { id: ID! itemsInStock: Int! sku: String! }

The second @key simply means that the inventory subgraph can also resolve a Product from its SKU.

Then a reviews subgraph for product reviews:

type Product @key(fields: "id") { id: ID! reviews: [Review!] }

And finally a search subgraph that can find products based on a query:

type Query { findProducts(searchQuery: String!): [Product!] } type Product @key(fields: "id") { id: ID! }

After composition, the API of the federated graph will look like this:

type Query { findProducts(searchQuery: String!): [Product!] } type Product { id: ID! reviews: [Review!] itemsInStock: Int! sku: String! }

From an API client's perspective, there is a single Product type. Each subgraph contributed fields to it transparently and without coordination.

directive @interfaceObject on OBJECT

Expresses that an object type is part of an Entity Interface. See the section on Entity Interfaces for details and examples.

directive @shareable on FIELD_DEFINITION | OBJECT

Declares a type or a field as shared between subgraphs. Unlike an entity defined with @key, shareable types and fields must be resolvable in all subgraphs. A good example would be a Color type:

type Color @shareable { red: Int! green: Int! blue: Int! }

Every subgraph that can return a Color is expected to provide all the fields. Another way to think about shareable types is to consider them like value types that are always available as a whole.

Annotating a type like Color in the example above is the same as annotating every field of that type with @shareable.

directive @requires(fields: FieldSet!) on FIELD_DEFINITION

Expresses the idea that a field requires other fields from the parent type that may be resolved in other subgraphs. For example, let us say that your hotel booking has a subgraph responsible for managing room service, but that subgraph does not have specific hotels in its database, it knows what room service is available depending on the location and category of the hotel, but it does not have a hotels database. We could use @requires in the following way.

In the Hotels subgraph:

type Hotel @key(fields: "id") { id: ID! category: Int countryCode: String }

and in the RoomService subgraph:

type Hotel @key(fields: "id") { id: ID! category: Int @external countryCode: String @external roomServiceOffering: [String!]! @requires(fields: "category countryCode") }

In this last snippet, the RoomService subgraph expresses that it can resolve Hotel.roomServiceOffering, but to do so, it needs the category and countryCode fields, resolved by an other subgraph. The dependency on the category and countryCode fields is expressed with @requires, and the fact that the subgraph cannot resolve them itself is expressed by @external. The required fields must be defined on the type, and they must be annotated with @external.

At runtime, when the gateway must resolve a query that selects Hotel.roomServiceOffering, it will know that it must first query the Hotels subgraph in order to pass them to the RoomService subgraph when resolving roomServiceOffering for that hotel. Diving a bit deeper, the mechanism by which the gateway does that is by passing the two previously retrieved fields to the entity resolver (Query._entities) on the RoomService subgraph.

Note that the fields marked with @external can be resolved by any number of other subgraphs: zero is valid but will make the field with @requires impossible to query, one if the fields are regular fields on the entity, or potentially more if the fields are @shareable.

Remember: @requires is only allowed on fields of entities, and only used in combination with @external.

directive @provides(fields: FieldSet!) on FIELD_DEFINITION

Written on a field, @provides communicates to the gateway that whenever the field is resolved, a set of other fields on the same object can also be resolved by the same subgraph, as an optimization. The other fields must be annotated with @external, because they cannot be resolved in the subgraph unless the field annotated with @provides is also resolved. You can think of this directive as a more restricted version of @shareable. Shareable fields can always be resolved, whereas fields marked with @external and provided with @provides are only resolvable when the providing field is also resolved.

A Farms subgraph:

type Farm @key(fields: "id") { id: ID! name: String! location: String vegetables: [Vegetable] @provides(fields: "name") } type Vegetable @key(fields: "id") { id: ID! name: String! @external } extend type Query { farm(id: ID!): Farm vegetablesInSeason(date: Date!): [Vegetable!] }

And a Veggies subgraph:

type Vegetable @key(fields: "id") { id: ID! name: String! scientificName: String! nutritionInfo: NutritionInfo marketPriceEur: Int }

Now let us look at what @provides does with two example queries.

query { farm(id: "6058691a-2d0a-47f1-95b3-1632f9ad16f9") { id name } }

Since the Query.farm field provides name, the gateway can resolve the whole query only talking to the Farms subgraph. On the other hand, in this second query:

query { vegetablesInSeason(date: "2023-10-03") { id name } }

the gateway will have to fetch the vegetable name from the Veggies subgraph. Note that this is transparent to the client: from an API consumer's perspective, there is only one Vegetable type with all the fields defined across all the subgraphs.

directive @external on FIELD_DEFINITION | OBJECT

On a field, expresses that a field is present in the subgraph's schema, but the subgraph cannot resolve it. This directive is only meaningful in combination with @provides and @requires.

Writing @external on an object is equivalent to writing @external on each of the object's fields.

directive @override(from: String!, label: String) on FIELD_DEFINITION

The use case for @override is migrating a field from one subgraph to another subgraph. Before you can remove the field from the first subgraph, you have to define it in the second subgraph, but the rules of composition will prevent you from defining the same field in two subgraphs. If you define the field as @shareable, it must be resolvable in all subgraphs, not only these two. You could mark the new field as @inaccessible at first, but you cannot switch the field over from the old to the new subgraph without coordination and downtime.

So another solution is needed, and the @override directive is that solution. When you annotate a field with @override(from: "other-subgraph"), the gateway will route requests for that field to the subgraph that defines the override and ignore that the field still exists in other-subgraph. That way, teams can deploy at their own pace with the following workflow (each bullet point is a publish):

  • Deploy the new field with @override in the overriding subgraph. That change can easily be reverted if the field does not behave as well in the next subgraph. Once the change is reverted, the gateway will go back to simply resolving the field in the original subgraph.
  • Remove the old field in the overriden subgraph.
  • Remove the @override directive in the overriding subgraph.

Imagine you are splitting the handling of comments away from your blog engine monolith into its own service, with its own subgraph. The Post type looks like this on the monolith:

type Post @key(fields: "id") { id: ID! title: String comments: [Comment!] publishedAt: DateTime author: User }

Migrating the Post.comments field to the new comments service can be achieved by adding the following to the comments subgraph's schema:

type Post @key(fields: "id") { id: ID! comments: [Comment!] @override(from: "monolith") }

After deploying this addition, all the traffic for Post.comments will be routed to the comments subgraph.

  • from: the name of the subgraph where the field is also defined, and which will be overriden by the field annotated with @override. Note that this is not validated to enable the following workflow: 1. define the overriding field with @override, 2. deploy, 3. remove the overriden field, 4. remove the @override on the new field. If composition validated that the field is also defined in the subgraph referenced in from, the migration would break at step (3.), since subgraphs are published independently.
  • label: allows partial or progressive overriding. The label argument takes a string of the form "percent(n)" where "n" is an integer from 0 to 100 specifying a percentage of the traffic for the field that the gateway should route to the overriding subgraph. In other words, @override(from: "inventory", label: "percent(0)"") will not change how the field is resolved, and @override(from: "inventory", label: "percent(100)") would be equivalent to @override(from: "inventory") without label argument.
directive @composeDirective(name: String!) repeatable on SCHEMA

By default, only some built-in (@deprecated) and Federation directives (e.g. @inaccessible) defined on subgraphs will be passed on by composition into the Federated Schema.

The @composeDirective directive is a signal for composition to preserve instances of a specific directive in the final API schema of the Federated Graph.

Arguments:

  • name: the name of the directive to compose.

Example:

extend schema @composeDirective(name: "myCustomDirective") type Test @myCustomDirective { id: ID! }
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM

Restrict access to the annotated item to successfully authenticated users. For more granularity, use @requiresScopes.

directive @requiresScopes( scopes: [[String!]!]! ) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM

Restrict access to the annotated item to users having a matching scope claim in their JWT access token. The scope claim is expected to be a string of space-separated scope names. The directive scopes argument is an array of arrays that should be understood as a list of combinations of scopes. Each inner array is a combination of scopes where all scopes must be present (AND). The outer array is a list of accepted alternative combinations of scopes (OR). The order is not relevant.

For example, imagine you want to restrict access to the view count for a blog post to either users with the editor and analytics scope, or admins:

type BlogPost { id: ID! title: String! author: User viewCount: Int @requiresScopes(scopes: [["admin"], ["editor", "analytics"]]) content: String }
directive @inaccessible on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

Parts of the schema that are marked as @inaccessible will not be part of the composed API schema exposed by the gateway. It only takes one @inaccessible in one subgraph to exclude an item from the composed API, even if the same item is not inaccessible in other subgraphs.

type PersonalDetails @inaccessible { age: Int heightCm: Int birday: Date } type User @key(fields: "id") @key(fields: "socialSecurityNumber") { id: ID! # socialSecurityNumber is a key to enable fetching users by social security number, but we do not want the field to be queryable. socialSecurityNumber: String! @inaccessible # This field must be marked as inaccessible because the field type is inaccessible. details: PersonalDetails @inaccessible }

Imagine you have an RGB color type shared across multiple subgraphs. Since the type is @shareable, every subgraph must be able to return all of its fields. Now you want to add a new field to that type, so you start adding it to a first subgraph, and then publish. This will lead to a composition error: one subgraph has the new field, but not others. You can accept this fact and publish all your subgraphs in close succession, accepting that your federated graph will not compose in the intermediate states. That works, but it scales poorly if you have many subgraphs managed by different teams. A better solution can be implemented with @inaccessible. You can publish the first subgraph adding the new field like this:

type Color @shareable { red: Int! green: Int! blue: Int! opacity: Int! @inaccessible }

Since opacity is inaccessible, it does not trigger a composition error, even if opacity is not defined in other subgraphs. You can progressively add the field to other subgraphs, and once they all define it, remove the @inaccessible. All the intermediate states compose cleanly.

Was this page helpful?