Complete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Complete Guide to GraphQL Federation
Introduction
GraphQL Federation lets you split a large GraphQL API across multiple services (subgraphs) while exposing a single unified API through a gateway. Each team owns their subgraph, defines their types, and the federation layer composes them into a supergraph. This guide covers subgraph setup, supergraph composition, entity resolution, and gateway deployment using Apollo Federation.
Federation Architecture
Client → Gateway (Supergraph) → Subgraph A (Users)
→ Subgraph B (Orders)
→ Subgraph C (Products)
- Subgraph: A GraphQL service owned by a team, defining part of the schema
- Supergraph: The composed schema from all subgraphs
- Gateway: The entry point that routes queries to the appropriate subgraphs
- Entity: A shared type with a key field that multiple subgraphs can reference and extend
Subgraph Setup
Users subgraph (Node.js)
const { buildSubgraphSchema } = require("@apollo/subgraph");
const { gql, ApolloServer } = require("apollo-server-express");
const typeDefs = gql`
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
extend type Order @key(fields: "id") {
id: ID! @external
user: User! @provides(fields: "name")
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
const resolvers = {
User: {
orders(user) {
return fetch(`http://orders-service/orders?userId=${user.id}`)
.then((res) => res.json());
},
},
Query: {
user: (_, { id }) => fetch(`http://users-service/users/${id}`).then((res) => res.json()),
users: () => fetch("http://users-service/users").then((res) => res.json()),
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
server.listen({ port: 4001 }).then(({ url }) => {
console.log(`Users subgraph ready at ${url}`);
});
Orders subgraph (Node.js)
const typeDefs = gql`
type Order @key(fields: "id") {
id: ID!
total: Float!
status: String!
userId: ID!
user: User!
items: [OrderItem!]!
}
type OrderItem {
productId: ID!
quantity: Int!
price: Float!
}
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]! @external
}
extend type Product @key(fields: "id") {
id: ID! @external
orders: [OrderItem!]!
}
type Query {
order(id: ID!): Order
orders: [Order!]!
}
type Mutation {
createOrder(userId: ID!, items: [OrderItemInput!]!): Order!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
`;
const resolvers = {
Order: {
user(order) {
return { __typename: "User", id: order.userId };
},
items(order) {
return order.items;
},
},
Product: {
orders(product) {
return fetch(`http://orders-service/orders/items?productId=${product.id}`)
.then((res) => res.json());
},
},
Query: {
order: (_, { id }) => fetch(`http://orders-service/orders/${id}`).then((res) => res.json()),
orders: () => fetch("http://orders-service/orders").then((res) => res.json()),
},
Mutation: {
createOrder: (_, { userId, items }) => {
return fetch("http://orders-service/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, items }),
}).then((res) => res.json());
},
},
};
Products subgraph (Python)
from ariadne import QueryType, make_federated_schema, ObjectType
from ariadne.asgi import GraphQL
import httpx
type_defs = """
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
description: String
}
extend type OrderItem @key(fields: "productId") {
productId: ID! @external
product: Product
}
type Query {
product(id: ID!): Product
products: [Product!]!
}
"""
query = QueryType()
product_obj = ObjectType("Product")
@query.field("product")
async def resolve_product(_, info, id):
async with httpx.AsyncClient() as client:
resp = await client.get(f"http://products-service/products/{id}")
return resp.json()
@query.field("products")
async def resolve_products(_, info):
async with httpx.AsyncClient() as client:
resp = await client.get("http://products-service/products")
return resp.json()
@product_obj.field("__resolve_reference")
async def resolve_product_reference(reference, info):
async with httpx.AsyncClient() as client:
resp = await client.get(f"http://products-service/products/{reference['id']}")
return resp.json()
schema = make_federated_schema(type_defs, [query, product_obj])
app = GraphQL(schema, debug=True)
Gateway Setup
const { ApolloGateway } = require("@apollo/gateway");
const { ApolloServer } = require("apollo-server-express");
const gateway = new ApolloGateway({
serviceList: [
{ name: "users", url: "http://localhost:4001/graphql" },
{ name: "orders", url: "http://localhost:4002/graphql" },
{ name: "products", url: "http://localhost:4003/graphql" },
],
debug: true,
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
server.listen({ port: 4000 }).then(({ url }) => {
console.log(`Gateway ready at ${url}`);
});
Supergraph Composition
# Install Rover CLI
curl -sSL https://rover.apollo.dev/nix/latest | sh
# Compose supergraph from subgraph schemas
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# supergraph.yaml
federation_version: =2.8.0
subgraphs:
users:
routing_url: http://localhost:4001/graphql
schema:
subgraph_url: http://localhost:4001/graphql
orders:
routing_url: http://localhost:4002/graphql
schema:
subgraph_url: http://localhost:4002/graphql
products:
routing_url: http://localhost:4003/graphql
schema:
subgraph_url: http://localhost:4003/graphql
Entity Resolution
Entities are the core of federation. They let subgraphs reference types owned by other subgraphs.
@key — define an entity
type User @key(fields: "id") {
id: ID!
name: String!
}
@extends — extend an entity from another subgraph
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
@requires — compute fields based on external fields
extend type Product @key(fields: "id") {
id: ID! @external
price: Float! @external
discountedPrice: Float! @requires(fields: "price")
}
@provides — indicate a subgraph can provide fields of another type
extend type Order @key(fields: "id") {
id: ID! @external
user: User! @provides(fields: "name")
}
@shareable — allow a field to be resolved by multiple subgraphs
type Product @key(fields: "id") {
id: ID! @shareable
name: String! @shareable
}
Querying the Federated Graph
# This query spans all three subgraphs:
# 1. Gateway sends user query to Users subgraph
# 2. Gateway sends orders query to Orders subgraph (using user.id as entity key)
# 3. Gateway sends product query to Products subgraph (using orderItem.productId as entity key)
query GetUserWithOrders {
user(id: "1") {
id
name
email
orders {
id
total
status
items {
quantity
product {
name
price
}
}
}
}
}
Best Practices
- One subgraph per team — ownership boundaries match team boundaries
- Use entities for shared types —
@keyon types referenced across subgraphs - Keep subgraphs independent — each subgraph should work standalone
- Use
@externalfor foreign fields — never duplicate field definitions - Avoid circular dependencies — subgraph A extends User, subgraph B extends Order, not both extending each other
- Use Rover for composition — validate schema changes before deploying
- Cache entity resolution — gateway calls
__resolveReferencefrequently - Monitor query plans — understand how the gateway splits queries across subgraphs
- Use managed federation (Apollo Studio) — track schema changes and composition errors
- Version subgraphs independently — the gateway handles composition, not individual subgraphs
- Handle subgraph failures gracefully — use partial results and error extensions
- Set timeouts on subgraph calls — one slow subgraph should not block the entire query
Common Mistakes
- Defining the same field in multiple subgraphs without
@shareable— composition fails - Not implementing
__resolveReference— entity lookups return null - Creating tight coupling between subgraphs — defeats the purpose of federation
- Not handling subgraph downtime — gateway errors instead of returning partial data
- Using
@requireswith non-external fields — composition validation fails - Not testing composition locally — schema conflicts surface only in production
- Overusing
@shareable— defeats ownership boundaries - Not monitoring query plan performance — N+1 entity resolution kills latency
- Exposing internal IDs across subgraph boundaries — leak implementation details
- Not using DataLoader for entity batching — one query triggers hundreds of subgraph calls
Frequently Asked Questions
What is the difference between schema stitching and federation?
Schema stitching manually combines schemas with custom resolvers. Federation uses a standardized protocol (@key, @extends, __resolveReference) so subgraphs declare their relationships declaratively. Federation is the recommended approach for new projects — it is more maintainable and has better tooling.
How does the gateway handle a query that spans multiple subgraphs?
The gateway builds a query plan. For a query fetching a user and their orders, it first calls the Users subgraph for the user, then uses the user’s id as an entity key to call the Orders subgraph. The gateway joins the results and returns a single response to the client.
Can I use federation without Apollo?
Yes. Federation is an open specification. Alternatives include Apollo Gateway (Node.js), Apollo Router (Rust), and custom gateways. The subgraph protocol is language-agnostic — you can build subgraphs in Python (Ariadne, Strawberry), Java (DGS), Go (gqlgen), and Ruby (graphql-ruby).