Docs
Launch GraphOS Studio
You're viewing documentation for a previous version of this software. Switch to the latest stable version.

Apollo Federation subgraph specification

For implementing subgraphs in other languages


This content is provided for developers adding federated support to a library, and for anyone curious about the inner workings of federation. It is not required if you're already using a subgraph-compatible library like .

Servers that are partially or fully compatible with this specification are tracked in Apollo's subgraph compatibility repository.

To make a service subgraph-capable, it needs the following:

  • Implementation of the federation schema specification
  • Support for fetching service capabilities
  • Implementation of stub type generation for references
  • Implementation of request resolving for entities.

Federation schema specification

Federated services will need to implement the following additions to the schema to allow the gateway to use the service for execution:

scalar _Any
scalar _FieldSet
# a union of all types that use the @key directive
union _Entity
type _Service {
sdl: String
}
extend type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
directive @external on FIELD_DEFINITION
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
# this is an optional directive discussed below
directive @extends on OBJECT | INTERFACE

For more information on these additions, see the glossary.

Fetch service capabilities

Schema at the gateway requires having each service's schema, annotated with its federation configuration. This information is fetched from each service using _service, an enhanced entry point added to the root of each federated service.

Note that the _service is not exposed by the gateway; it is solely for internal use.

The _service should return the _Service type which has a single field called sdl. This () is a printed version of the service's schema including the annotations of federation . This SDL does not include the additions of the federation spec above. Given an input like this:

extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
}

The generated SDL should match that exactly with no additions. It is important to preserve the type and locations and to omit the federation types.

Some libraries such as graphql-java don't have native support for type extensions in their printer. supports using an @extends directive in place of extend type to annotate type references:

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

Create stub types

Individual federated services should be runnable without having the entire present. marked with @external are declarations of that are defined in another service. All fields referred to in @key, @requires, and @provides directives need to have corresponding @external fields in the same service. This allows us to be explicit about dependencies on another service, and service composition will verify that an @external field matches up with the original field definition, which can catch mistakes or migration issues (when the original field changes its type for example). @external fields also give an individual service the type information it needs to validate and decode incoming representations (this is especially important for custom ), without requiring the composed graph schema to be available at runtime in each service.

A federated service should take the @external fields and types and create them locally so the service can run on its own.

Resolve requests for entities

Execution of a federated graph requires being able to "enter" into a service at an type. To do this, federated services need to do two things:

  • Make each entity in the schema part of the _Entity union
  • Implement the _entities field on the query root

To implement the _Entity union, each type annotated with @key should be added to the _Entity union. If no types are annotated with the key directive, then the _Entity union and Query._entities field should be removed from the schema. For example, given the following partial schema:

type Review @key(fields: "id") {
id: ID!
body: String
author: User
product: Product
}
extend type User @key(fields: "email") {
email: String! @external
}
extend type Product @key(fields: "upc") {
upc: String! @external
}

The _Entity union for that partial schema should be the following:

union _Entity = Review | User | Product

The _Entity union is critical to support the _entities root field:

_entities(representations: [_Any!]!): [_Entity]!

Queries across service boundaries will start off from the _entities root field. The resolver for this field receives a list of representations. A representation is a blob of data that is supposed to match the combined requirements of the fields requested on an entity.

For example, if we execute a query for the top product's reviews:

query GetTopProductReviews {
topProducts {
reviews {
body
}
}
}

The gateway will first fetch the topProducts from the Products service, asking for the upc of each product:

query {
topProducts {
upc
}
}

The reason it requests upc is because that field is specified as a requirement on reviews:

extend type Product @key(fields: "upc") {
upc: String @external
reviews: [Review]
}

The gateway will then send a list of representations for the fetched products to the Reviews service:

{
"query": ...,
"variables": {
"_representations": [
{
"__typename": "Product",
"upc": "B00005N5PF"
},
...
]
}
}
query ($_representations: [_Any!]!) {
_entities(representations: $_representations) {
... on Product {
reviews {
body
}
}
}
}

GraphQL execution will then go over each representation in the list, use the __typename to match type conditions, build up a merged selection set, and execute it. Here, the inline on Product will match and the reviews resolver will be called repeatedly with the representation for each product. Since Product is part of the _Entity union, it can be selected as a return of the _entities resolver.

To ensure the required fields are provided, and of the right type, the source object properties should coerce the input into the expected type for the property (by calling parseValue on the type).

The real resolver will then be able to access the required properties from the (partial) object:

{
Product: {
reviews(object) {
return fetchReviewsForProductWithUPC(object.upc);
}
}
}

Schema modifications glossary

type _Service

A new called _Service must be created. This type must have an sdl: String! field which exposes the SDL of the service's schema

Query._service

A new field must be added to the query root called _service. This field must return a non-nullable _Service type. The _service field on the query root must return SDL which includes all of the service's types (after any non-federation transforms), as well as federation directive annotations on the fields and types. The federation schema modifications (i.e. new types and directive definitions) should not be included in this SDL.

union _Entity

A new union called _Entity must be created. This should be a union of all types that use the @key directive, including both types native to the schema and extended types.

scalar _Any

A new scalar called _Any must be created. The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema.

scalar _FieldSet

A new scalar called _FieldSet is a custom scalar type that is used to represent a set of fields. Grammatically, a field set is a selection set minus the braces. This means it can represent a single field "upc", multiple fields "id countryCode", and even nested selection sets "id organization { id }".

Query._entities

A new field must be added to the query root called _entities. This field must return a non-nullable list of _Entity types and have a single with an argument name of representations and type [_Any!]! (non-nullable list of non-nullable _Any scalars). The _entities field on the query root must allow a list of _Any scalars which are "representations" of entities from external services. These representations should be validated with the following rules:

  • Any representation without a __typename: String field is invalid.
  • Representations must contain at least the fields defined in the fieldset of a @key directive on the base type.

@key

directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE

The @key directive is used to indicate a combination of fields that can be used to uniquely identify and fetch an object or interface.

type Product @key(fields: "upc") {
upc: UPC!
name: String
}

Multiple keys can be defined on a single object type:

type Product @key(fields: "upc") @key(fields: "sku") {
upc: UPC!
sku: SKU!
name: String
}

Note: Repeated directives (in this case, @key, used multiple times) require support by the underlying GraphQL implementation.

@provides

directive @provides(fields: _FieldSet!) on FIELD_DEFINITION

The @provides directive is used to annotate the expected returned from a field on a base type that is guaranteed to be selectable by the gateway. Given the following example:

type Review @key(fields: "id") {
product: Product @provides(fields: "name")
}
extend type Product @key(fields: "upc") {
upc: String @external
name: String @external
}

When fetching Review.product from the Reviews service, it is possible to request the name with the expectation that the Reviews service can provide it when going from review to product. Product.name is an external field on an external type which is why the local type extension of Product and annotation of name is required.

@requires

directive @requires(fields: _FieldSet!) on FIELD_DEFINITION

The @requires directive is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a where the required fields may not be needed by the client, but the service may need additional information from other services. For example:

# extended from the Users service
extend type User @key(fields: "id") {
id: ID! @external
email: String @external
reviews: [Review] @requires(fields: "email")
}

In this case, the Reviews service adds new capabilities to the User type by providing a list of reviews related to a user. In order to fetch these reviews, the Reviews service needs to know the email of the User from the Users service in order to look up the reviews. This means the reviews field / resolver requires the email field from the base User type.

@external

directive @external on FIELD_DEFINITION

The @external directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. For example:

# extended from the Users service
extend type User @key(fields: "email") {
email: String @external
reviews: [Review]
}

This type extension in the Reviews service extends the User type from the Users service. It extends it for the purpose of adding a new field called reviews, which returns a list of Reviews.

Previous
Subgraph-compatible libraries
Next
Overview
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company