GraphQL solved the over-fetching and under-fetching problems of REST. But as adoption grows, a single GraphQL server becomes a bottleneck. Federation allows multiple teams to contribute to a unified graph while maintaining ownership of their domains.
Here’s how to implement GraphQL federation.
The Single Graph Problem
What Goes Wrong
monolithic_graph_problems:
team_coupling:
- All changes go through one team
- Bottleneck for schema changes
- Coordination overhead
deployment_coupling:
- All resolvers in one service
- Can't deploy independently
- Blast radius is entire API
ownership_unclear:
- Who owns user.orders?
- Schema design by committee
- Quality inconsistent
Federation Solution
┌─────────────────────────────────────────────────────────────────┐
│ Gateway │
│ (Apollo Gateway / Router) │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Users │ │ Orders │ │Products │
│Subgraph │ │Subgraph │ │Subgraph │
└─────────┘ └─────────┘ └─────────┘
│ │ │
│ Owns │ Owns │
▼ ▼ ▼
User type Order type Product type
User.id Order.id Product.id
User.email Order.items Product.name
User.profile Order.customer Product.price
Federation Concepts
Entities
Entities are types that can be referenced across subgraphs:
# Users subgraph
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
profile: Profile
}
# Orders subgraph extends User
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
recentOrder: Order
}
# Reviews subgraph extends User
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float
}
Key Directive
# Single key
type Product @key(fields: "id") {
id: ID!
name: String!
}
# Multiple keys (different ways to identify)
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String!
}
# Compound key
type ProductVariant @key(fields: "productId variantId") {
productId: ID!
variantId: ID!
color: String
size: String
}
Entity Resolution
// Users subgraph resolver
const resolvers = {
User: {
// Entity reference resolver
__resolveReference(user: { id: string }) {
return userService.getById(user.id);
},
},
Query: {
user: (_, { id }) => userService.getById(id),
me: (_, __, { userId }) => userService.getById(userId),
},
};
// Orders subgraph resolver
const resolvers = {
User: {
// Extends User with orders
orders: (user: { id: string }) => {
return orderService.getByUserId(user.id);
},
recentOrder: (user: { id: string }) => {
return orderService.getMostRecent(user.id);
},
},
Order: {
__resolveReference(order: { id: string }) {
return orderService.getById(order.id);
},
customer: (order) => {
// Return reference for gateway to resolve
return { __typename: 'User', id: order.customerId };
},
},
};
Implementation
Subgraph Setup
// Users subgraph
import { ApolloServer } from '@apollo/server';
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 {
user(id: ID!): User
me: User
}
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: DateTime!
}
`;
const resolvers = {
Query: {
user: (_, { id }) => userService.getById(id),
me: (_, __, context) => userService.getById(context.userId),
},
User: {
__resolveReference: ({ id }) => userService.getById(id),
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
Gateway Configuration
// Apollo Router (Rust-based, production recommended)
// router.yaml
supergraph:
listen: 0.0.0.0:4000
introspection: true
headers:
all:
request:
- propagate:
matching: "x-.*"
- propagate:
named: "authorization"
cors:
origins:
- https://app.example.com
allow_credentials: true
# Supergraph composition
# supergraph.yaml
federation_version: =2.0.0
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
subgraph_url: http://users-service:4001/graphql
orders:
routing_url: http://orders-service:4002/graphql
schema:
subgraph_url: http://orders-service:4002/graphql
products:
routing_url: http://products-service:4003/graphql
schema:
subgraph_url: http://products-service:4003/graphql
Schema Composition
# Compose supergraph schema
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql
# Or use Apollo Studio managed federation
rover subgraph publish my-graph@current \
--name users \
--schema ./users/schema.graphql \
--routing-url http://users-service:4001/graphql
Query Execution
How Federation Resolves
# Client query
query GetUserWithOrders {
user(id: "123") {
id
name # Users subgraph
email # Users subgraph
orders { # Orders subgraph
id
total
items {
product { # Products subgraph
name
price
}
}
}
}
}
execution_plan:
1. Gateway receives query
2. Query plan generated:
- Fetch User from Users subgraph
- Fetch orders for User from Orders subgraph
- Fetch products for items from Products subgraph
3. Execute in optimal order (parallel where possible)
4. Merge results
5. Return to client
Schema Design
Ownership Boundaries
ownership_principles:
own_your_types:
- Define types you're authoritative for
- Users subgraph owns User
- Orders subgraph owns Order
extend_others:
- Add fields to types you don't own
- Orders adds User.orders
- Reviews adds Product.reviews
reference_entities:
- Return references for cross-graph relationships
- Order.customer returns User reference
- Don't duplicate resolver logic
Shared Types
# Use @shareable for types multiple subgraphs define
type Money @shareable {
amount: Float!
currency: String!
}
# Or define in one place and reference
# Products defines Price
type Product @key(fields: "id") {
id: ID!
price: Money!
}
# Orders references it
extend type Product @key(fields: "id") {
id: ID! @external
price: Money! @external # Use external from products
}
Production Considerations
Performance
performance_tips:
batching:
- Use DataLoader for entity resolution
- Batch requests to same subgraph
caching:
- Cache entity references
- Consider CDN caching for public data
- Apollo Studio automatic persisted queries
query_planning:
- Review query plans for expensive queries
- Optimize subgraph data access
- Consider query complexity limits
// DataLoader for batching
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await userService.getByIds(ids);
return ids.map(id => users.find(u => u.id === id));
});
const resolvers = {
User: {
__resolveReference: ({ id }, context) => {
return context.loaders.userLoader.load(id);
},
},
};
Observability
tracing:
- Trace across subgraphs
- Include subgraph name in spans
- Track entity resolution latency
metrics:
- Query latency by operation
- Subgraph latency
- Error rates by subgraph
- Cache hit rates
logging:
- Operation name and variables
- Subgraph requests
- Error details with context
Schema Evolution
schema_evolution:
breaking_changes:
- Run composition on PR
- Fail if breaking changes detected
- Require explicit approval for breaking
versioning:
- Prefer additive changes
- Deprecate before removing
- Use @deprecated directive
ci_cd:
- Validate schema on commit
- Run composition in CI
- Deploy subgraph first, then update registry
Key Takeaways
- Federation enables multiple teams to contribute to a unified graph
- Each subgraph owns its types and extends others
- Entities are types that can be referenced across subgraphs
- Gateway composes schema and orchestrates queries
- Use @key to define how entities are identified
- __resolveReference resolves entity references in subgraphs
- Design for clear ownership boundaries
- Use DataLoader for batching entity resolution
- Schema composition in CI catches breaking changes
- Apollo Router provides production-grade gateway
Federation isn’t just technical—it’s organizational. Teams own their domains and can move independently while clients see a unified API.