The Microservices API Challenge
GraphQL Federation enables you to build a unified GraphQL API from multiple underlying services, each managing their own portion of the graph. This approach solves the complex challenge of creating cohesive APIs in microservices architectures while maintaining team autonomy and service independence.
At ScriptLabs Studios, we've implemented GraphQL Federation for enterprises managing dozens of microservices, creating seamless API experiences that scale to hundreds of millions of requests per day.
Understanding Apollo Federation 2.0
Apollo Federation 2.0 introduces powerful composition features that make it easier to build and maintain distributed GraphQL schemas at scale.
Core Federation Concepts
- Subgraphs - Individual GraphQL services that own specific entities
- Supergraph - The unified schema composed from all subgraphs
- Router - The gateway that executes queries across subgraphs
- Federation directives - Special directives that define relationships
- Entity resolution - How the router fetches related data
Subgraph Architecture
Users Subgraph
# users/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: DateTime!
profile: UserProfile
}
type UserProfile {
bio: String
avatar: String
location: String
}
// users/resolvers.js
const resolvers = {
User: {
__resolveReference(user) {
return findUserById(user.id);
},
profile: async (user) => {
return await fetchUserProfile(user.id);
}
},
Query: {
me: (_, __, { user }) => {
return user;
},
users: async (_, { limit, offset }) => {
return await fetchUsers({ limit, offset });
}
},
Mutation: {
updateProfile: async (_, { input }, { user }) => {
const updatedUser = await updateUserProfile(user.id, input);
return updatedUser;
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginInlineTrace()],
});
Products Subgraph
# products/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])
type Product @key(fields: "id") {
id: ID!
name: String!
description: String!
price: Money!
category: Category!
reviews: [Review!]!
createdBy: User! # Reference to Users subgraph
}
type Category @key(fields: "id") {
id: ID!
name: String!
products: [Product!]!
}
type Money {
amount: Float!
currency: String!
}
type Review {
id: ID!
rating: Int!
comment: String!
reviewer: User! # Reference to Users subgraph
createdAt: DateTime!
}
# Extend User from users subgraph
extend type User @key(fields: "id") {
id: ID! @external
createdProducts: [Product!]!
reviews: [Review!]!
}
// products/resolvers.js
const resolvers = {
Product: {
__resolveReference(product) {
return findProductById(product.id);
},
category: async (product) => {
return await fetchCategory(product.categoryId);
},
reviews: async (product) => {
return await fetchProductReviews(product.id);
},
createdBy: (product) => {
return { __typename: "User", id: product.createdById };
}
},
Category: {
__resolveReference(category) {
return findCategoryById(category.id);
},
products: async (category) => {
return await fetchProductsByCategory(category.id);
}
},
User: {
createdProducts: async (user) => {
return await fetchProductsByUser(user.id);
},
reviews: async (user) => {
return await fetchReviewsByUser(user.id);
}
},
Review: {
reviewer: (review) => {
return { __typename: "User", id: review.reviewerId };
}
},
Query: {
product: (_, { id }) => findProductById(id),
products: (_, args) => fetchProducts(args),
categories: () => fetchAllCategories()
}
};
Advanced Federation Patterns
Entity Interfaces and Unions
# shared/schema.graphql
interface Node @key(fields: "id") {
id: ID!
}
union SearchResult = User | Product | Category
# products/schema.graphql
type Product implements Node @key(fields: "id") {
id: ID!
name: String!
# ... other fields
}
# users/schema.graphql
type User implements Node @key(fields: "id") {
id: ID!
email: String!
# ... other fields
}
# search/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external"])
extend type Query {
search(query: String!): [SearchResult!]!
node(id: ID!): Node
}
Computed Fields with @requires
# inventory/schema.graphql
extend type Product @key(fields: "id") {
id: ID! @external
price: Money! @external
# Computed field that requires price from products subgraph
discountedPrice: Money! @requires(fields: "price")
inStock: Boolean!
availableQuantity: Int!
}
// inventory/resolvers.js
const resolvers = {
Product: {
discountedPrice: (product) => {
const discount = calculateDiscount(product.price, product.category);
return {
amount: product.price.amount * (1 - discount),
currency: product.price.currency
};
},
inStock: async (product) => {
const inventory = await fetchInventory(product.id);
return inventory.quantity > 0;
},
availableQuantity: async (product) => {
const inventory = await fetchInventory(product.id);
return inventory.quantity;
}
}
};
Apollo Router Configuration
Router Setup with Composition
# router/supergraph.yaml
federation_version: 2
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
subgraph_url: http://users-service:4001/graphql
products:
routing_url: http://products-service:4002/graphql
schema:
subgraph_url: http://products-service:4002/graphql
inventory:
routing_url: http://inventory-service:4003/graphql
schema:
subgraph_url: http://inventory-service:4003/graphql
reviews:
routing_url: http://reviews-service:4004/graphql
schema:
subgraph_url: http://reviews-service:4004/graphql
# router/router.yaml
supergraph:
path: ./supergraph-schema.graphql
headers:
all:
request:
- propagate:
named: "authorization"
- propagate:
named: "x-user-id"
cors:
origins:
- "https://app.example.com"
allow_headers:
- "content-type"
- "authorization"
plugins:
- name: "authentication"
enabled: true
- name: "authorization"
enabled: true
- name: "caching"
enabled: true
traffic_shaping:
all:
timeout: 30s
subgraphs:
users:
timeout: 5s
products:
timeout: 10s
Automated Schema Composition
#!/bin/bash
# compose-schema.sh
# Install Rover CLI
npm install -g @apollo/rover
# Compose supergraph schema
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
if [ $? -eq 0 ]; then
echo "Schema composition successful"
# Validate the composed schema
rover graph introspect --schema ./supergraph-schema.graphql
# Start the router
./router --config router.yaml --supergraph supergraph-schema.graphql
else
echo "Schema composition failed"
exit 1
fi
Performance Optimization
Query Planning and Caching
// router/plugins/caching.js
const cache = new Map();
const cachingPlugin = {
requestDidStart() {
return {
willSendSubgraphRequest({ request, subgraphName }) {
const cacheKey = subgraphName + ':' + request.query + ':' + JSON.stringify(request.variables);
if (cache.has(cacheKey)) {
const cachedResponse = cache.get(cacheKey);
if (Date.now() - cachedResponse.timestamp < 60000) { // 1 minute TTL
return cachedResponse.response;
}
}
return null;
},
didReceiveSubgraphResponse({ response, subgraphName, request }) {
if (response.data && !response.errors) {
const cacheKey = subgraphName + ':' + request.query + ':' + JSON.stringify(request.variables);
cache.set(cacheKey, {
response: response.data,
timestamp: Date.now()
});
}
}
};
}
};
DataLoader Implementation
// Implement DataLoader in subgraphs for batching
const DataLoader = require('dataloader');
class ProductService {
constructor() {
this.productLoader = new DataLoader(this.batchLoadProducts.bind(this));
this.categoryLoader = new DataLoader(this.batchLoadCategories.bind(this));
}
async batchLoadProducts(productIds) {
const products = await db.product.findMany({
where: { id: { in: productIds } }
});
const productMap = products.reduce((map, product) => {
map[product.id] = product;
return map;
}, {});
return productIds.map(id => productMap[id] || null);
}
async batchLoadCategories(categoryIds) {
const categories = await db.category.findMany({
where: { id: { in: categoryIds } }
});
const categoryMap = categories.reduce((map, category) => {
map[category.id] = category;
return map;
}, {});
return categoryIds.map(id => categoryMap[id] || null);
}
// Use loaders in resolvers
async findProductById(id) {
return this.productLoader.load(id);
}
async findCategoryById(id) {
return this.categoryLoader.load(id);
}
}
const productService = new ProductService();
const resolvers = {
Product: {
category: async (product) => {
return await productService.findCategoryById(product.categoryId);
}
}
};
Monitoring and Observability
Distributed Tracing
// monitoring/tracing.js
const { NodeTracerProvider } = require('@opentelemetry/sdk-node');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'graphql-federation',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
});
// Add to Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginInlineTrace(),
{
requestDidStart() {
return {
willSendSubgraphRequest({ request, subgraphName }) {
const span = trace.getActiveSpan();
span?.setAttributes({
'subgraph.name': subgraphName,
'graphql.operation.type': request.operationName,
});
},
didReceiveSubgraphResponse({ response, subgraphName }) {
const span = trace.getActiveSpan();
span?.setAttributes({
'subgraph.response.errors': response.errors?.length || 0,
});
}
};
}
}
]
});
Metrics Collection
// monitoring/metrics.js
const promClient = require('prom-client');
const metrics = {
httpRequestDuration: new promClient.Histogram({
name: 'graphql_request_duration_seconds',
help: 'GraphQL request duration',
labelNames: ['operation', 'subgraph']
}),
subgraphRequests: new promClient.Counter({
name: 'graphql_subgraph_requests_total',
help: 'Total subgraph requests',
labelNames: ['subgraph', 'status']
}),
entityResolutions: new promClient.Counter({
name: 'graphql_entity_resolutions_total',
help: 'Total entity resolutions',
labelNames: ['entity_type']
})
};
const metricsPlugin = {
requestDidStart() {
return {
willSendSubgraphRequest({ subgraphName }) {
const timer = metrics.httpRequestDuration.startTimer({
subgraph: subgraphName
});
return {
didReceiveSubgraphResponse({ response }) {
timer();
metrics.subgraphRequests.inc({
subgraph: subgraphName,
status: response.errors ? 'error' : 'success'
});
}
};
}
};
}
};
Security Implementation
Authentication and Authorization
// auth/middleware.js
const jwt = require('jsonwebtoken');
class AuthenticationService {
async validateToken(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return {
userId: decoded.sub,
roles: decoded.roles,
permissions: decoded.permissions
};
} catch (error) {
throw new Error('Invalid token');
}
}
async checkPermission(user, resource, action) {
return user.permissions.some(permission =>
permission.resource === resource &&
permission.actions.includes(action)
);
}
}
// Apply to subgraph resolvers
const resolvers = {
Query: {
sensitiveData: async (_, args, { user }) => {
if (!user) {
throw new Error('Authentication required');
}
if (!await authService.checkPermission(user, 'sensitive_data', 'read')) {
throw new Error('Insufficient permissions');
}
return await fetchSensitiveData(args);
}
}
};
Schema-Level Security
# auth/schema.graphql
directive @auth(requires: [String!]) on FIELD_DEFINITION | OBJECT
type User @auth(requires: ["user:read"]) {
id: ID!
email: String! @auth(requires: ["user:email"])
personalData: PersonalData! @auth(requires: ["user:pii"])
}
type AdminPanel @auth(requires: ["admin:access"]) {
users: [User!]!
metrics: SystemMetrics!
}
Deployment Strategies
Container Orchestration
# docker-compose.yml
version: '3.8'
services:
apollo-router:
image: ghcr.io/apollographql/router:latest
ports:
- "4000:4000"
volumes:
- ./router/router.yaml:/app/router.yaml
- ./router/supergraph-schema.graphql:/app/supergraph-schema.graphql
environment:
- APOLLO_ROUTER_CONFIG_PATH=/app/router.yaml
- APOLLO_ROUTER_SUPERGRAPH_PATH=/app/supergraph-schema.graphql
depends_on:
- users-service
- products-service
- inventory-service
users-service:
build: ./services/users
ports:
- "4001:4001"
environment:
- DATABASE_URL=${USERS_DATABASE_URL}
- APOLLO_KEY=${APOLLO_KEY}
products-service:
build: ./services/products
ports:
- "4002:4002"
environment:
- DATABASE_URL=${PRODUCTS_DATABASE_URL}
- APOLLO_KEY=${APOLLO_KEY}
inventory-service:
build: ./services/inventory
ports:
- "4003:4003"
environment:
- DATABASE_URL=${INVENTORY_DATABASE_URL}
- APOLLO_KEY=${APOLLO_KEY}
Kubernetes Deployment
# k8s/router-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: apollo-router
spec:
replicas: 3
selector:
matchLabels:
app: apollo-router
template:
metadata:
labels:
app: apollo-router
spec:
containers:
- name: router
image: ghcr.io/apollographql/router:latest
ports:
- containerPort: 4000
env:
- name: APOLLO_ROUTER_CONFIG_PATH
value: /app/router.yaml
- name: APOLLO_ROUTER_SUPERGRAPH_PATH
value: /app/supergraph-schema.graphql
volumeMounts:
- name: config
mountPath: /app
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: config
configMap:
name: router-config
---
apiVersion: v1
kind: Service
metadata:
name: apollo-router-service
spec:
selector:
app: apollo-router
ports:
- protocol: TCP
port: 80
targetPort: 4000
type: LoadBalancer
Testing Federation
Integration Testing
// tests/federation.test.js
const { ApolloServer } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
describe('Federation Integration Tests', () => {
let usersServer, productsServer;
beforeAll(async () => {
// Start subgraph servers
usersServer = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs: usersTypeDefs, resolvers: usersResolvers })
});
productsServer = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs: productsTypeDefs, resolvers: productsResolvers })
});
await usersServer.listen({ port: 4001 });
await productsServer.listen({ port: 4002 });
});
afterAll(async () => {
await usersServer.stop();
await productsServer.stop();
});
test('should resolve cross-subgraph queries', async () => {
const query = `
query GetUserWithProducts($userId: ID!) {
user(id: $userId) {
id
name
email
createdProducts {
id
name
price {
amount
currency
}
}
}
}
`;
const response = await request(app)
.post('/graphql')
.send({
query,
variables: { userId: '1' }
});
expect(response.status).toBe(200);
expect(response.body.data.user).toBeDefined();
expect(response.body.data.user.createdProducts).toBeDefined();
});
});
Migration Strategies
From Monolith to Federation
// Migration phases
const migrationPhases = [
{
phase: 1,
description: "Extract Users subgraph",
steps: [
"Create users service with GraphQL endpoint",
"Implement user-related resolvers",
"Set up federation directives",
"Test user queries in isolation"
]
},
{
phase: 2,
description: "Extract Products subgraph",
steps: [
"Create products service",
"Implement product resolvers",
"Add User extensions for product relationships",
"Test federated user-product queries"
]
},
{
phase: 3,
description: "Extract remaining domains",
steps: [
"Identify remaining domain boundaries",
"Extract services incrementally",
"Maintain backwards compatibility",
"Gradual traffic migration"
]
}
];
// Incremental router configuration
const routerConfig = {
// Start with monolith handling most traffic
subgraphs: {
monolith: {
routing_url: "http://monolith:4000/graphql",
schema: {
subgraph_url: "http://monolith:4000/graphql"
}
},
users: {
routing_url: "http://users-service:4001/graphql",
schema: {
subgraph_url: "http://users-service:4001/graphql"
}
}
}
};
Best Practices
Schema Design Guidelines
- Entity Ownership - Each entity should be owned by exactly one subgraph
- Key Fields - Use stable, unique identifiers for @key directives
- Field Placement - Place fields close to their data source
- Avoid Deep Nesting - Prefer flatter structures for better performance
- Consistent Naming - Use consistent field and type names across subgraphs
Performance Considerations
- Query Planning - Understand how the router plans query execution
- N+1 Queries - Use DataLoader to batch entity resolutions
- Caching Strategy - Implement appropriate caching at multiple levels
- Monitoring - Track subgraph performance and query complexity
Conclusion
GraphQL Federation with Apollo Router provides a powerful solution for building unified APIs across microservices architectures. By enabling teams to own their domain-specific subgraphs while presenting a cohesive API to consumers, Federation strikes the right balance between autonomy and consistency.
The key to successful Federation lies in thoughtful schema design, proper entity modeling, and robust operational practices. With careful planning and implementation, Federation can scale to support the largest and most complex microservices environments.
As organizations continue to adopt microservices architectures, GraphQL Federation represents a mature, production-ready approach to API composition that can grow with your needs while maintaining developer experience and performance.
Ready to implement GraphQL Federation for your microservices architecture? Let's discuss how Apollo Federation can unify your APIs while preserving team autonomy and system scalability.
