Graphql Federation Patterns

Production engineering guide for graphql federation patterns covering patterns, implementation strategies, and operational best practices.

GraphQL Federation enables a monolithic GraphQL schema to be composed from multiple independently developed services, each responsible for a domain-specific subset of the overall API. It matters when teams own distinct data domains—user profiles, product catalog, order processing—and need to expose a unified schema without tight coupling or repeated schema duplication. Federation becomes essential when schema evolution, deployment cadence, and ownership diverge across services.

Schema Composition and Service Discovery

A federated GraphQL system begins with a gateway that aggregates queries across member services. The gateway discovers services via a well-known endpoint, typically at /_service, and fetches each service’s schema via the GET /_service HTTP request. The response must be a valid GraphQL SDL (Schema Definition Language) document.

Each service advertises its capabilities using directives. The @extends directive defines that a type from another service is being extended, while @key declares how a type is uniquely identified across the federation.


type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
  posts: [Post] @relationship
}

type Post @key(fields: "id") {
  id: ID!
  title: String!
  author: User!
}

The @key directive is critical: it defines the primary key fields for a type, enabling the gateway to resolve cross-service fields. The gateway uses the @key to locate the service that owns a type and fetch it.

When a client queries user(id: 1) { name, posts { title } }, the gateway resolves User from the user service, then resolves posts by fetching each Post from the post service. The gateway generates a JOIN of the two services’ schemas.

The @key directive and its failure modes

The @key directive is not just metadata—it drives the entire query execution. The fields argument must be a space-separated list of field names, and the gateway uses these to build a lookup map.

type Product @key(fields: "sku brand") {
  sku: String!
  brand: String!
  name: String!
  price: Float!
}

If a service returns Product with sku: "A123" and brand: "Acme", the gateway uses (sku, brand) as the composite key to route queries to the product service.

A common failure: the gateway expects a key field to be non-null, but the service returns null for a key field. The gateway logs:

Missing key field 'brand' in Product with sku="A123", null value found

This triggers a fallback: the gateway attempts to resolve the Product via sku only, but this can lead to incorrect or duplicated data if multiple products share the same sku but different brand.

Another silent failure: a service adds a new @key field without updating the @extends directive. The gateway discovers the new key but does not know how to merge it with existing @key definitions. The result is a “partial key” mismatch.

Field Resolution and Subgraph Discovery

When the gateway receives a query, it first identifies which services are needed to resolve the requested fields. It uses the @key and @extends directives to build a dependency graph.

For each @key type, the gateway queries each service’s _service endpoint to build a catalog of known types and their owners.

GET /_service
Accept: application/json

The response must be a JSON object with a schema field containing the SDL.

{
  "schema": "type User @key(fields: \"id\") { id: ID! name: String! }"
}

The gateway also checks for @requires and @provides directives to determine how to split a query across services.

@requires and @provides for efficient query execution

The @requires directive specifies the fields needed to resolve a field, while @provides lists the fields that a service can resolve.

type User {
  id: ID!
  name: String!
  posts: [Post] @relationship
}

type Post {
  id: ID!
  title: String!
  author: User!
  metadata: PostMetadata @provides(fields: "title author")
}

type PostMetadata {
  publishedAt: DateTime!
  tags: [String!]!
}

When a client queries:

{
  user(id: 1) {
    name
    posts {
      title
      metadata {
        tags
      }
    }
  }
}

The gateway identifies that User is owned by the user service, Post by the post service, and PostMetadata by the metadata service.

The @provides directive allows the gateway to coalesce queries. The Post service can resolve metadata fields, so the gateway sends:

query {
  user(id: 1) {
    name
    posts {
      id
      title
      metadata {
        tags
      }
    }
  }
}

But it also sees that Post has author and title, so it can use @provides to avoid redundant fetches.

The gateway then resolves metadata by querying the metadata service with id from Post.

The @requires directive and its pitfalls

The @requires directive is used to define dependencies between fields. A @requires(fields: "...") on a field indicates that the field can only be resolved if the listed fields are available.

type User {
  id: ID!
  name: String!
  posts: [Post] @requires(fields: "id name")
  recentActivity: [Activity] @requires(fields: "id")
}

Here, posts depends on id and name being available. The gateway must ensure that both are resolved before querying for posts.

A failure occurs when a service returns a User object missing name, but posts is requested. The gateway logs:

Field 'posts' requires 'name' but it is missing in User with id=1

The gateway then fetches name from the user service and retries the posts query.

A subtle issue: @requires fields are resolved in the *context* of the parent object. If a Post has author of type User, and User has posts, then posts depends on id and name of User. When resolving posts on Post, the gateway must first resolve the User object, then resolve posts using the User's id and name.

If a service returns a User with id but not name, the posts field is marked as pending, and the gateway sends a second request to fetch name.

Type Stitching and Schema Merging

Federation enables type stitching: a type defined across multiple services can be resolved by combining data from different sources.

type Product {
  id: ID!
  name: String!
  seller: Seller!
}

type Seller {
  id: ID!
  name: String!
  location: String!
}

Suppose Product is defined in the product service and Seller in the seller service. The product service uses @extends to import Seller:

extend type Seller @key(fields: "id") {
  id: ID!
  name: String!
  location: String!
}

When a client queries:

{
  product(id: 1) {
    name
    seller {
      name
      location
    }
  }
}

The gateway first resolves Product from the product service. It then sees that seller is a Seller object, and uses the @key to route seller to the seller service.

The gateway resolves seller with id from Product, then fetches name and location from the seller service.

The @external directive and its edge cases

The @external directive marks a field as being external to the current service. It is used in @extends to define the shape of a type that is partially defined in the current service.

extend type Product @key(fields: "id") {
  id: ID! @external
  name: String! @external
  seller: Seller! @external
  reviews: [Review]!
}

Here, id and name are external fields: they are not computed by the product service but come from another source. The gateway uses the @key and @external to determine how to resolve the Product object.

A common mistake: a service defines Product with id as @external, but forgets to mark name as @external, even though name is also provided by another service. The gateway resolves Product from the product service, but name is not part of the external contract.

A silent failure: the gateway assumes that id is always available, but a service returns a Product with id: null. The gateway logs:

External field 'id' is null in Product, cannot resolve via @key

The gateway then falls back to the product service to resolve Product by id, but this leads to incorrect or missing data.

Handling schema changes and versioning

When a service updates its schema, the gateway must detect changes. The gateway caches the _service response and invalidates it based on ETag or Last-Modified.

A service may return a new @key or add a new @requires field. The gateway automatically discovers it on the next query.

A failure mode: a service adds a new @provides field but does not update its _service endpoint. The gateway continues to use the old schema and misses the new field.

Another failure: a service changes the @key of a type, but the old key fields are not updated. The gateway resolves Product using the old key, but some Product objects have the new key format, leading to inconsistent results.

The gateway logs:

Mismatch between key fields and actual key values: expected (sku, brand), found (sku, brand, category)

This indicates that the service now uses a composite key including category, but the gateway expects only sku and brand.

Query Optimization and Error Handling

The gateway can optimize queries by batching requests. It uses a GraphQL request with operationName, variables, and query fields.

When resolving User, the gateway batches multiple User queries into a single request:

{
  "operationName": "GetUsers",
  "variables": {
    "ids": [1, 2, 3]
  },
  "query": "query GetUsers($ids: [ID!]!) { users(ids: $ids) { id name } }"
}

A failure in the gateway: a service returns an error field in its response, but the gateway does not propagate it to the client.

{
  "data": {
    "user": {
      "id": 1,
      "name": "Alice",
      "posts": null
    }
  },
  "errors": [
    {
      "message": "Post service unavailable",
      "path": ["user", "posts"],
      "extensions": {
        "code": "SERVICE_UNAVAILABLE",
        "service": "post-service"
      }
    }
  ]
}

The client receives the partial data and an error, but the error is not always surfaced correctly.

A common failure: the gateway does not handle @key resolution for nested types correctly. When User has a posts field that returns Post, and Post has an author of type User, the gateway must resolve User for each Post. If the author field is missing, the gateway logs:

Missing field 'author' in Post with id=1, cannot resolve via @key

This leads to a full query execution delay.

Summary of Key Patterns

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.