Federation and Microservices β
TL;DR β
GraphQL Federation allows multiple GraphQL services to compose into a single unified graph. Each service owns its portion of the schema and can extend types from other services. Apollo Federation is the most mature implementation, using a gateway (router) to orchestrate queries across services. This enables teams to work independently while providing a seamless API to clients.
The Problem Federation Solves β
Monolithic GraphQL Challenges β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Monolithic GraphQL Problems β
β β
β Single GraphQL Server β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Giant Monolith β β
β β β β
β β Users Posts Products Orders ... β β
β β Schema Schema Schema Schema β β
β β Resolvers Resolvers Resolvers Resolvers β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Problems: β
β β’ Single team owns entire schema β
β β’ Deployments affect entire API β
β β’ Schema conflicts between teams β
β β’ Scaling requires scaling everything β
β β’ Testing complexity grows exponentially β
β β’ Cannot use different languages/frameworks β
β β
β Federation Solution: β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β
β β Users β β Posts β β Products β β Orders β β
β β Service β β Service β β Service β β Service β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ β
β β β β β β
β ββββββββββββββ΄βββββββββββββ΄βββββββββββββ β
β β β
β βββββββββ΄ββββββββ β
β β Gateway β β
β β (Router) β β
β βββββββββββββββββ β
β β β
β Clients β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββApollo Federation Concepts β
Architecture Overview β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Federation Architecture β
β β
β βββββββββββββββββββ β
β β Client β β
β ββββββββββ¬βββββββββ β
β β β
β ββββββββββ΄βββββββββ β
β β Apollo Router β β
β β (Gateway) β β
β β β β
β β β’ Query Planningβ β
β β β’ Orchestration β β
β β β’ Response Mergeβ β
β ββββββββββ¬βββββββββ β
β β β
β βββββββββββββββββββββΌββββββββββββββββββββ β
β β β β β
β βββββββββ΄ββββββββ ββββββββ΄βββββββ βββββββββ΄ββββββββ β
β β Users β β Posts β β Products β β
β β Subgraph β β Subgraph β β Subgraph β β
β β β β β β β β
β β type User β β type Post β β type Product β β
β β @key(id) β β @key(id) β β @key(sku) β β
β β β β β β β β
β β extend Post β β extend User β β β β
β β author β β posts β β β β
β βββββββββββββββββ βββββββββββββββ βββββββββββββββββ β
β β
β Each subgraph: β
β β’ Owns its types β
β β’ Can extend other types β
β β’ Deployed independently β
β β’ Different teams/languages β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββKey Directives β
graphql
# @key - Defines the entity's primary key for cross-service references
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
# Multiple keys supported
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String!
price: Float!
}
# @external - Field is defined in another subgraph
type User @key(fields: "id") {
id: ID!
# These fields come from Users subgraph
name: String! @external
}
# @requires - This field needs external fields to resolve
type User @key(fields: "id") {
id: ID!
name: String! @external
# Needs name from Users service to compute
greeting: String! @requires(fields: "name")
}
# @provides - Specifies fields this resolver will return
type Review @key(fields: "id") {
id: ID!
body: String!
# This resolver provides author.name
author: User! @provides(fields: "name")
}
# @shareable - Field can be resolved by multiple subgraphs
type Product @key(fields: "id") {
id: ID!
name: String! @shareable
price: Float!
}
# @inaccessible - Hide field from composed schema
type User @key(fields: "id") {
id: ID!
name: String!
internalId: String! @inaccessible # Not exposed to clients
}
# @override - Take ownership of a field from another subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
# Products service now owns this field
inventory: Int! @override(from: "inventory")
}Implementing Subgraphs β
Users Subgraph β
javascript
// users-subgraph/schema.graphql
const { gql } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
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
users: [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
avatar: String
createdAt: DateTime!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
avatar: String
}
`;
const resolvers = {
Query: {
me: (_, __, context) => context.dataSources.users.getUser(context.userId),
user: (_, { id }, context) => context.dataSources.users.getUser(id),
users: (_, __, context) => context.dataSources.users.getAllUsers(),
},
Mutation: {
createUser: (_, { input }, context) =>
context.dataSources.users.createUser(input),
updateUser: (_, { id, input }, context) =>
context.dataSources.users.updateUser(id, input),
},
User: {
// Reference resolver - called when another subgraph needs User
__resolveReference: (user, context) => {
return context.dataSources.users.getUser(user.id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});Posts Subgraph (Extends User) β
javascript
// posts-subgraph/schema.graphql
const typeDefs = gql`
extend schema @link(
url: "https://specs.apollo.dev/federation/v2.0"
import: ["@key", "@external", "@requires"]
)
type Query {
post(id: ID!): Post
posts(authorId: ID): [Post!]!
feed(first: Int, after: String): PostConnection!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
authorId: ID!
createdAt: DateTime!
# Reference to User entity (resolved by Users subgraph)
author: User!
}
# Extend User type defined in Users subgraph
extend type User @key(fields: "id") {
id: ID! @external
# Add posts field to User
posts: [Post!]!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
input CreatePostInput {
title: String!
content: String!
}
`;
const resolvers = {
Query: {
post: (_, { id }, context) =>
context.dataSources.posts.getPost(id),
posts: (_, { authorId }, context) =>
authorId
? context.dataSources.posts.getPostsByAuthor(authorId)
: context.dataSources.posts.getAllPosts(),
},
Mutation: {
createPost: (_, { input }, context) =>
context.dataSources.posts.createPost({
...input,
authorId: context.userId,
}),
},
Post: {
__resolveReference: (post, context) =>
context.dataSources.posts.getPost(post.id),
// Return reference to User (gateway will resolve)
author: (post) => ({ __typename: 'User', id: post.authorId }),
},
// Resolver for extended User type
User: {
posts: (user, _, context) =>
context.dataSources.posts.getPostsByAuthor(user.id),
},
};Reviews Subgraph (Extends Product and User) β
javascript
const typeDefs = gql`
extend schema @link(
url: "https://specs.apollo.dev/federation/v2.0"
import: ["@key", "@external", "@provides"]
)
type Query {
reviews(productId: ID!): [Review!]!
}
type Mutation {
createReview(input: CreateReviewInput!): Review!
}
type Review @key(fields: "id") {
id: ID!
rating: Int!
body: String!
createdAt: DateTime!
# References
author: User!
product: Product!
}
# Extend Product from Products subgraph
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float
}
# Extend User from Users subgraph
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
input CreateReviewInput {
productId: ID!
rating: Int!
body: String!
}
`;
const resolvers = {
Query: {
reviews: (_, { productId }, context) =>
context.dataSources.reviews.getReviewsForProduct(productId),
},
Mutation: {
createReview: (_, { input }, context) =>
context.dataSources.reviews.createReview({
...input,
authorId: context.userId,
}),
},
Review: {
__resolveReference: (review, context) =>
context.dataSources.reviews.getReview(review.id),
author: (review) => ({ __typename: 'User', id: review.authorId }),
product: (review) => ({ __typename: 'Product', id: review.productId }),
},
Product: {
reviews: (product, _, context) =>
context.dataSources.reviews.getReviewsForProduct(product.id),
averageRating: async (product, _, context) => {
const reviews = await context.dataSources.reviews
.getReviewsForProduct(product.id);
if (!reviews.length) return null;
const sum = reviews.reduce((acc, r) => acc + r.rating, 0);
return sum / reviews.length;
},
},
User: {
reviews: (user, _, context) =>
context.dataSources.reviews.getReviewsByAuthor(user.id),
},
};Gateway (Router) Setup β
Apollo Router β
yaml
# router.yaml
supergraph:
introspection: true
listen: 0.0.0.0:4000
# Subgraph configuration
override_subgraph_url:
users: http://users-service:4001/graphql
posts: http://posts-service:4002/graphql
reviews: http://reviews-service:4003/graphql
products: http://products-service:4004/graphql
# Headers propagation
headers:
all:
request:
- propagate:
named: authorization
- propagate:
named: x-request-id
# Caching
cache:
redis:
urls:
- redis://redis:6379
# Rate limiting
limits:
max_depth: 15
max_height: 200
# Telemetry
telemetry:
tracing:
otlp:
endpoint: http://jaeger:4317Supergraph Composition β
bash
# Install rover CLI
npm install -g @apollo/rover
# Compose supergraph from subgraph schemas
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql
# supergraph.yaml
federation_version: =2.3.1
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
file: ./users/schema.graphql
posts:
routing_url: http://posts-service:4002/graphql
schema:
file: ./posts/schema.graphql
reviews:
routing_url: http://reviews-service:4003/graphql
schema:
file: ./reviews/schema.graphql
products:
routing_url: http://products-service:4004/graphql
schema:
file: ./products/schema.graphqlManaged Federation (Apollo Studio) β
javascript
// Publish schema to Apollo Studio
// Each subgraph publishes independently
// In CI/CD for users-subgraph:
// rover subgraph publish my-graph@production \
// --name users \
// --schema ./schema.graphql \
// --routing-url http://users-service:4001/graphql
// Router fetches composed supergraph from Apollo Studio
// router.yaml
apollo:
graph_ref: my-graph@production
// Benefits:
// - Schema validation before deploy
// - Schema change history
// - Breaking change detection
// - Composition errors caught earlyQuery Execution β
Query Planning β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Query Execution Flow β
β β
β Client Query: β
β { β
β user(id: "1") { β
β name # Users subgraph β
β email # Users subgraph β
β posts { # Posts subgraph β
β title β
β reviews { # Reviews subgraph β
β rating β
β } β
β } β
β reviews { # Reviews subgraph β
β rating β
β product { # Products subgraph β
β name β
β } β
β } β
β } β
β } β
β β
β Query Plan: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. Fetch user from Users β β
β β βββΊ { id, name, email } β β
β β β β
β β 2. Parallel: β β
β β βββΊ Fetch posts from Posts (user.id) β β
β β β βββΊ { id, title, authorId } β β
β β β β β
β β βββΊ Fetch reviews from Reviews (user.id) β β
β β βββΊ { id, rating, productId } β β
β β β β
β β 3. Parallel: β β
β β βββΊ Fetch reviews for posts from Reviews β β
β β βββΊ Fetch products from Products β β
β β β β
β β 4. Merge results β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββEntity Resolution β
javascript
// When gateway needs to resolve User reference from Posts subgraph
// 1. Posts subgraph returns:
{
"posts": [
{ "id": "1", "title": "Hello", "author": { "__typename": "User", "id": "100" } }
]
}
// 2. Gateway sends _entities query to Users subgraph:
query {
_entities(representations: [
{ "__typename": "User", "id": "100" }
]) {
... on User {
id
name
email
}
}
}
// 3. Users subgraph __resolveReference is called:
User: {
__resolveReference: (ref, context) => {
// ref = { __typename: "User", id: "100" }
return context.dataSources.users.getUser(ref.id);
}
}
// 4. Gateway merges response:
{
"posts": [
{
"id": "1",
"title": "Hello",
"author": { "id": "100", "name": "Alice", "email": "alice@example.com" }
}
]
}Migration Strategy β
Incremental Adoption β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Migration Path β
β β
β Phase 1: Gateway in front of monolith β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Gateway β β
β β β β β
β β ββββββββ΄βββββββ β β
β β β Monolith β (All types) β β
β β β Subgraph β β β
β β βββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Phase 2: Extract first service β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Gateway β β
β β β β β
β β ββββββββ΄βββββββ¬ββββββββββββββββ β β
β β β Monolith β Users β β β
β β β (reduced) β Subgraph β β β
β β βββββββββββββββ΄ββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Phase 3: Continue extraction β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Gateway β β
β β β β β
β β ββββββββ΄βββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ β β
β β β Legacy β Users β Posts β Products β β β
β β β (minimal) β β β β β β
β β βββββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Phase 4: Decomposition complete β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Gateway β β
β β β β β
β β ββββββββ΄βββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ β β
β β β Users β Posts β Products β Reviews β β β
β β βββββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββUsing @override for Migration β
graphql
# Step 1: Original in Monolith
# monolith/schema.graphql
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
inventory: Int! # Currently in monolith
}
# Step 2: New Inventory service takes over
# inventory/schema.graphql
type Product @key(fields: "id") {
id: ID! @external
# Take ownership from monolith
inventory: Int! @override(from: "monolith")
}
# Step 3: Remove from monolith after migration complete
# monolith/schema.graphql
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
# inventory removed
}Error Handling β
Partial Responses β
javascript
// Gateway handles partial failures gracefully
// Query:
{
user(id: "1") {
name # Users service
posts { # Posts service (fails)
title
}
reviews { # Reviews service
rating
}
}
}
// Response with partial failure:
{
"data": {
"user": {
"name": "Alice",
"posts": null, // Failed
"reviews": [
{ "rating": 5 }
]
}
},
"errors": [
{
"message": "Could not fetch posts",
"path": ["user", "posts"],
"extensions": {
"code": "SUBGRAPH_ERROR",
"serviceName": "posts"
}
}
]
}
// Client can display partial data with error noticeSubgraph Health Checks β
javascript
// Health check endpoint in each subgraph
app.get('/health', async (req, res) => {
const checks = {
database: await checkDatabase(),
cache: await checkCache(),
};
const healthy = Object.values(checks).every(c => c);
res.status(healthy ? 200 : 503).json({
status: healthy ? 'healthy' : 'unhealthy',
checks,
});
});
// Router health aggregation
// router.yaml
health_check:
enabled: true
path: /health
subgraphs:
- users
- posts
- productsPerformance Considerations β
Batching Entity Requests β
javascript
// Multiple entity references batched into single request
// Instead of:
// _entities(representations: [{id: "1"}])
// _entities(representations: [{id: "2"}])
// _entities(representations: [{id: "3"}])
// Gateway sends:
// _entities(representations: [{id: "1"}, {id: "2"}, {id: "3"}])
// Subgraph should use DataLoader pattern
User: {
__resolveReference: async (ref, context) => {
// Uses DataLoader to batch
return context.loaders.users.load(ref.id);
}
}Query Plan Caching β
yaml
# router.yaml
# Cache query plans for repeated queries
query_planning:
cache:
in_memory:
limit: 1000
redis:
urls:
- redis://redis:6379
ttl: 3600Deferred Execution β
graphql
# Use @defer for expensive cross-service fields
query GetUser {
user(id: "1") {
name
email
... @defer {
posts {
title
# Requires Posts service
}
reviews {
rating
# Requires Reviews service
}
}
}
}Best Practices β
Schema Design β
β‘ One entity per service (ownership clear)
β‘ Use @key on all entities
β‘ Prefer extending over sharing types
β‘ Keep entity references minimal
β‘ Document service boundaries
β‘ Version schema changes carefullyService Design β
β‘ Each subgraph independently deployable
β‘ Use DataLoader in __resolveReference
β‘ Implement health checks
β‘ Handle partial failures gracefully
β‘ Log cross-service request traces
β‘ Monitor subgraph latenciesOperational β
β‘ Use managed federation for schema registry
β‘ Validate schema changes in CI
β‘ Monitor composition errors
β‘ Set up alerts for subgraph failures
β‘ Implement circuit breakers at gateway
β‘ Cache query plans and entity resolutions