Courses / GraphQL Complete Guide
Lesson 15 of 18
GraphQL Federation for Microservices
Build a unified GraphQL API from multiple microservices using Apollo Federation, subgraphs, and gateway composition.
What you'll learn
- ✓How Apollo Federation composes multiple subgraphs into one API
- ✓How to define and extend entities across services
- ✓How to set up a gateway with Apollo Router
- ✓How to handle cross-service relationships with @key and @requires
- ✓How to migrate from a monolith to federated architecture
Prerequisites
- •Solid understanding of GraphQL schemas and resolvers
- •Experience with microservice architectures
The Problem with Monolithic GraphQL
A single GraphQL server works well early on. One schema, one set of resolvers, one deployment. But as your application grows, this monolith becomes a bottleneck. Every team changes the same codebase. Deployments require coordination. A bug in one resolver can take down the entire API.
Apollo Federation solves this by letting each team own a subgraph — an independent GraphQL service that manages its own slice of the schema. A gateway composes these subgraphs into a single unified API that clients query as if it were one server.
Federation Architecture
The architecture has two layers:
Subgraphs are individual GraphQL services, each owning specific types and fields. The Users service owns User, the Products service owns Product, and the Reviews service owns Review.
The Gateway (Apollo Router) sits in front of all subgraphs. It fetches schemas from each subgraph, composes them into a supergraph, and routes incoming queries to the appropriate subgraphs. Clients never talk to subgraphs directly.
Setting Up a Subgraph
Each subgraph uses @apollo/subgraph to mark its schema as federation-ready. Start with the Users subgraph:
npm install @apollo/server @apollo/subgraph graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import gql from 'graphql-tag';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type Query {
me: User
user(id: ID!): User
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
role: String!
}
`;
const resolvers = {
Query: {
me: (_, __, { userId, db }) => db.users.findById(userId),
user: (_, { id }, { db }) => db.users.findById(id),
},
User: {
__resolveReference: (reference, { db }) => {
// Called when another subgraph references a User by its key
return db.users.findById(reference.id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
});
console.log(`Users subgraph running at ${url}`);
The @key(fields: "id") directive marks User as an entity — a type that can be referenced by other subgraphs. The __resolveReference resolver tells Federation how to fetch a User when another subgraph needs to resolve one.
Extending Entities Across Services
Now create a Products subgraph that references User:
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Query {
product(id: ID!): Product
products(limit: Int): [Product!]!
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
weight: Float!
seller: User!
}
type User @key(fields: "id") {
id: ID!
}
`;
const resolvers = {
Query: {
product: (_, { id }, { db }) => db.products.findById(id),
products: (_, { limit }, { db }) => db.products.findAll({ limit }),
},
Product: {
seller: (product) => {
// Return a reference -- the gateway will resolve the rest
// from the Users subgraph
return { __typename: 'User', id: product.sellerId };
},
},
User: {
__resolveReference: (ref) => ref,
},
};
The Products subgraph defines a stub User type with only the id field. It returns a reference object with __typename and the key field. The gateway recognizes this reference and calls the Users subgraph’s __resolveReference to fill in the remaining fields like name and email.
The Reviews Subgraph
The Reviews subgraph extends both User and Product:
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Review @key(fields: "id") {
id: ID!
body: String!
rating: Int!
author: User!
product: Product!
}
type User @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
averageRating: Float
}
type Query {
review(id: ID!): Review
}
`;
const resolvers = {
Query: {
review: (_, { id }, { db }) => db.reviews.findById(id),
},
User: {
reviews: (user, _, { db }) => db.reviews.findByAuthor(user.id),
},
Product: {
reviews: (product, _, { db }) => db.reviews.findByProduct(product.id),
averageRating: async (product, _, { db }) => {
const reviews = await db.reviews.findByProduct(product.id);
if (reviews.length === 0) return null;
const sum = reviews.reduce((acc, r) => acc + r.rating, 0);
return sum / reviews.length;
},
},
Review: {
author: (review) => ({ __typename: 'User', id: review.authorId }),
product: (review) => ({ __typename: 'Product', id: review.productId }),
},
};
The Reviews subgraph adds reviews and averageRating fields to types it does not own. Federation merges these contributions into the final composed schema.
Setting Up the Gateway
Apollo Router is the recommended gateway. It is a high-performance Rust binary that replaces the older @apollo/gateway Node.js package:
curl -sSL https://router.apollo.dev/download/nix/latest | sh
Create a supergraph.yaml configuration file:
federation_version: =2.0.0
subgraphs:
users:
routing_url: http://localhost:4001/graphql
schema:
subgraph_url: http://localhost:4001/graphql
products:
routing_url: http://localhost:4002/graphql
schema:
subgraph_url: http://localhost:4002/graphql
reviews:
routing_url: http://localhost:4003/graphql
schema:
subgraph_url: http://localhost:4003/graphql
Compose the supergraph and start the router:
rover supergraph compose --config supergraph.yaml > supergraph.graphql
./router --supergraph supergraph.graphql --dev
Alternatively, use the Node.js gateway for environments where the Rust router is not an option:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'products', url: 'http://localhost:4002/graphql' },
{ name: 'reviews', url: 'http://localhost:4003/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Gateway running at ${url}`);
Advanced Federation Directives
Federation provides several directives for controlling how types and fields are shared:
@key defines the primary key for entity lookups. An entity can have multiple keys:
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String!
}
@external marks a field as owned by another subgraph. You reference it but do not resolve it:
type Product @key(fields: "id") {
id: ID!
weight: Float @external
shippingCost: Float @requires(fields: "weight")
}
@requires declares that a field needs external fields to compute its value. The gateway fetches weight from the Products subgraph before calling the Shipping subgraph’s shippingCost resolver:
const resolvers = {
Product: {
shippingCost: (product) => {
// product.weight is populated by the gateway before this runs
return calculateShipping(product.weight);
},
},
};
@provides hints to the gateway that a subgraph can resolve fields from a related entity, reducing network hops:
type Review @key(fields: "id") {
id: ID!
product: Product! @provides(fields: "name price")
}
@shareable allows multiple subgraphs to resolve the same field. Without it, only one subgraph can own a field.
Query Planning
When a client sends a query, the gateway builds a query plan — a DAG of fetch operations across subgraphs. For a query like:
query {
products(limit: 5) {
name
price
seller {
name
email
}
reviews {
rating
body
}
}
}
The gateway generates a plan:
- Fetch
productsfrom the Products subgraph (returnsname,price, and seller references) - In parallel: fetch
Userdetails from Users subgraph AND fetchreviewsfrom Reviews subgraph - Merge everything into a single response
Understanding query plans helps you design subgraph boundaries for minimal cross-service hops.
Migration Strategy
Migrating from a monolith to federation works best incrementally:
-
Start with the gateway. Deploy a gateway in front of your monolith as a single subgraph. Clients now talk to the gateway, and nothing changes functionally.
-
Extract one service. Pick a bounded context (e.g., Reviews) and build it as a separate subgraph. Move the relevant types and resolvers.
-
Update the supergraph. Add the new subgraph to the gateway configuration. Remove the extracted types from the monolith subgraph.
-
Repeat. Extract the next bounded context. Each step is independently deployable and testable.
// During migration, the monolith subgraph gradually shrinks
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'monolith', url: 'http://localhost:4001/graphql' },
{ name: 'reviews', url: 'http://localhost:4002/graphql' },
// More subgraphs added over time
],
}),
});
Common Pitfalls
Circular dependencies. If Service A needs data from Service B and Service B needs data from Service A, you end up with circular fetches. Break cycles by restructuring entity boundaries or introducing a dedicated subgraph for the shared concern.
Over-fetching across boundaries. A query that touches many subgraphs results in multiple network hops. Group related fields in the same subgraph when possible.
Schema composition errors. Conflicting type definitions across subgraphs will fail composition. Run rover supergraph compose in CI to catch these errors before deployment.
Missing __resolveReference. Every entity type must implement __resolveReference or the gateway cannot fetch it. This is the most common source of null results in federated queries.
Federation adds operational complexity, but it gives teams autonomy, independent deployability, and a unified API surface that scales with your organization.
Progress is saved locally to your browser.