GraphQL Federation: Building Scalable Microservices with Unified Graph APIs
As organizations scale their applications, the microservices architecture has become the de facto standard for building maintainable, scalable systems. However, this approach often leads to API sprawl and complexity for frontend developers. GraphQL Federation emerges as a powerful solution, enabling teams to build a unified GraphQL API while maintaining the autonomy and benefits of microservices.
Understanding GraphQL Federation
GraphQL Federation allows you to compose a distributed graph from multiple GraphQL services. Each service owns its part of the graph and can be developed, deployed, and scaled independently while presenting a single, unified API to clients.
Traditional Microservices vs. Federation
Traditional Approach: Federation Approach:
┌─────────────┐ ┌─────────────────┐
│ Client │ │ Client │
└──────┬──────┘ └────────┬────────┘
│ │
┌───┴────┐ │
│Gateway │ │
└───┬────┘ ┌───────┴────────┐
┌────┼────┬─────┐ │ Apollo Gateway │
│ │ │ │ └───────┬────────┘
┌─┴─┐┌─┴─┐┌─┴─┐┌──┴──┐ │
│API││API││API││ API │ ┌────────┼────────┐
└───┘└───┘└───┘└─────┘ │ │ │
┌──┴──┐ ┌──┴──┐ ┌─┴──┐
│Users│ │Posts│ │Comm│
└─────┘ └─────┘ └────┘
Setting Up GraphQL Federation
1. Gateway Service
First, let's create the Apollo Gateway that will compose our federated graph:
// gateway/index.js
import { ApolloServer } from '@apollo/server';
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { startStandaloneServer } from '@apollo/server/standalone';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'posts', url: 'http://localhost:4002/graphql' },
{ name: 'comments', url: 'http://localhost:4003/graphql' },
],
}),
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Gateway ready at ${url}`);
2. User Service
Create the first federated service for user management:
// services/users/index.js
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable", "@external", "@provides"])
type User @key(fields: "id") {
id: ID!
username: String!
email: String!
profile: UserProfile!
createdAt: String!
}
type UserProfile {
bio: String
avatar: String
location: String
}
type Query {
user(id: ID!): User
users: [User!]!
currentUser: User
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
input CreateUserInput {
username: String!
email: String!
profile: ProfileInput
}
input UpdateUserInput {
username: String
email: String
profile: ProfileInput
}
input ProfileInput {
bio: String
avatar: String
location: String
}
`;
const resolvers = {
User: {
__resolveReference(user) {
return users.find(u => u.id === user.id);
},
},
Query: {
user: (_, { id }) => users.find(user => user.id === id),
users: () => users,
currentUser: (_, __, context) => {
return users.find(user => user.id === context.userId);
},
},
Mutation: {
createUser: (_, { input }) => {
const user = {
id: String(users.length + 1),
...input,
createdAt: new Date().toISOString(),
};
users.push(user);
return user;
},
updateUser: (_, { id, input }) => {
const index = users.findIndex(user => user.id === id);
if (index === -1) throw new Error('User not found');
users[index] = { ...users[index], ...input };
return users[index];
},
},
};
// In-memory data store
const users = [
{
id: '1',
username: 'alice',
email: '[email protected]',
profile: {
bio: 'Software Engineer',
avatar: 'https://example.com/alice.jpg',
location: 'San Francisco',
},
createdAt: '2024-01-01T00:00:00Z',
},
];
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
});
console.log(`🚀 Users service ready at ${url}`);
3. Posts Service
Create a service that references users:
// services/posts/index.js
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable", "@external", "@requires"])
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
publishedAt: String
updatedAt: String!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
postCount: Int!
}
type Query {
post(id: ID!): Post
posts(limit: Int = 10, offset: Int = 0): PostConnection!
postsByUser(userId: ID!): [Post!]!
searchPosts(query: String!): [Post!]!
}
type PostConnection {
nodes: [Post!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
endCursor: String
startCursor: String
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
`;
const resolvers = {
Post: {
__resolveReference(post) {
return posts.find(p => p.id === post.id);
},
author(post) {
return { __typename: 'User', id: post.authorId };
},
},
User: {
posts(user) {
return posts.filter(post => post.authorId === user.id);
},
postCount(user) {
return posts.filter(post => post.authorId === user.id).length;
},
},
Query: {
post: (_, { id }) => posts.find(post => post.id === id),
posts: (_, { limit, offset }) => {
const paginatedPosts = posts.slice(offset, offset + limit);
return {
nodes: paginatedPosts,
totalCount: posts.length,
pageInfo: {
hasNextPage: offset + limit < posts.length,
hasPreviousPage: offset > 0,
endCursor: Buffer.from(`${offset + limit}`).toString('base64'),
startCursor: Buffer.from(`${offset}`).toString('base64'),
},
};
},
postsByUser: (_, { userId }) =>
posts.filter(post => post.authorId === userId),
searchPosts: (_, { query }) =>
posts.filter(post =>
post.title.toLowerCase().includes(query.toLowerCase()) ||
post.content.toLowerCase().includes(query.toLowerCase())
),
},
Mutation: {
createPost: (_, { input }, context) => {
const post = {
id: String(posts.length + 1),
...input,
authorId: context.userId,
publishedAt: null,
updatedAt: new Date().toISOString(),
};
posts.push(post);
return post;
},
updatePost: (_, { id, input }) => {
const index = posts.findIndex(post => post.id === id);
if (index === -1) throw new Error('Post not found');
posts[index] = {
...posts[index],
...input,
updatedAt: new Date().toISOString(),
};
return posts[index];
},
deletePost: (_, { id }) => {
const index = posts.findIndex(post => post.id === id);
if (index === -1) return false;
posts.splice(index, 1);
return true;
},
publishPost: (_, { id }) => {
const post = posts.find(p => p.id === id);
if (!post) throw new Error('Post not found');
post.publishedAt = new Date().toISOString();
return post;
},
},
};
// In-memory data store
const posts = [
{
id: '1',
title: 'Introduction to GraphQL Federation',
content: 'GraphQL Federation is a powerful architecture...',
authorId: '1',
tags: ['graphql', 'federation', 'microservices'],
publishedAt: '2024-01-15T00:00:00Z',
updatedAt: '2024-01-15T00:00:00Z',
},
];
const server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4002 },
});
console.log(`🚀 Posts service ready at ${url}`);
Advanced Federation Patterns
1. Entity Resolution with External Fields
# In the reviews service
extend type User @key(fields: "id") {
id: ID! @external
username: String! @external
reviews: [Review!]!
averageRating: Float! @requires(fields: "username")
}
type Review @key(fields: "id") {
id: ID!
rating: Int!
comment: String!
author: User!
product: Product!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float!
}
2. Value Types and Shared Types
// Shared value types across services
const sharedTypeDefs = gql`
scalar DateTime
scalar JSON
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type Money @shareable {
amount: Float!
currency: String!
}
type Address @shareable {
street: String!
city: String!
country: String!
postalCode: String!
}
enum Status @shareable {
ACTIVE
INACTIVE
PENDING
ARCHIVED
}
`;
3. Custom Directives
// Custom federation directives
const typeDefs = gql`
directive @auth(requires: Role!) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: String!) on FIELD_DEFINITION
directive @deprecated(reason: String!) on FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
extend type Query {
adminUsers: [User!]! @auth(requires: ADMIN)
userAnalytics: Analytics! @auth(requires: ADMIN) @rateLimit(max: 100, window: "1h")
}
`;
// Implementing custom directives
const authDirective = {
auth: (next, _, { requires }, context) => {
if (!context.user || !context.user.roles.includes(requires)) {
throw new Error('Unauthorized');
}
return next();
},
};
Performance Optimization
1. DataLoader for N+1 Prevention
import DataLoader from 'dataloader';
// User service with DataLoader
const createUserLoader = () => new DataLoader(async (userIds) => {
const users = await db.users.findMany({
where: { id: { in: userIds } },
});
// Map results to match input order
const userMap = new Map(users.map(user => [user.id, user]));
return userIds.map(id => userMap.get(id));
});
const resolvers = {
Post: {
author: (post, _, context) => {
return context.loaders.user.load(post.authorId);
},
},
};
// Context factory
const createContext = ({ req }) => ({
userId: req.headers.authorization?.split(' ')[1],
loaders: {
user: createUserLoader(),
post: createPostLoader(),
},
});
2. Query Planning and Caching
// Gateway with query plan caching
import { ApolloGateway, RemoteGraphQLDataSource } from '@apollo/gateway';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({ request, context }) {
// Forward auth headers to subgraphs
request.http.headers.set('authorization', context.authorization);
request.http.headers.set('x-user-id', context.userId);
}
}
const gateway = new ApolloGateway({
supergraphSdl: await loadSupergraphSdl(),
buildService({ url }) {
return new AuthenticatedDataSource({ url });
},
queryPlannerConfig: {
cache: new InMemoryLRUCache({
maxSize: Math.pow(2, 20) * 100, // 100MB
ttl: 300, // 5 minutes
}),
},
});
3. Field-Level Metrics
// Tracking field usage and performance
import { Plugin } from '@apollo/server';
const fieldUsagePlugin: Plugin = {
requestDidStart() {
return {
willResolveField(fieldContext) {
const start = Date.now();
return (error, result) => {
const duration = Date.now() - start;
const { fieldName, parentType } = fieldContext.info;
// Track metrics
metrics.fieldDuration.observe(
{ field: fieldName, type: parentType.name },
duration
);
if (error) {
metrics.fieldErrors.inc({
field: fieldName,
type: parentType.name,
error: error.message,
});
}
};
},
};
},
};
Security Considerations
1. Authentication and Authorization
// Centralized auth in gateway
const authPlugin = {
requestDidStart() {
return {
async willSendRequest(requestContext) {
const { request, context } = requestContext;
// Verify JWT token
const token = request.http.headers.get('authorization');
if (token) {
try {
const user = await verifyToken(token);
context.user = user;
context.userId = user.id;
} catch (error) {
throw new AuthenticationError('Invalid token');
}
}
},
};
},
};
// Field-level authorization
const resolvers = {
User: {
email: (user, _, context) => {
// Only show email to self or admin
if (context.userId === user.id || context.user?.role === 'ADMIN') {
return user.email;
}
return null;
},
},
};
2. Rate Limiting
import { RateLimiterMemory } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemory({
points: 100, // Number of requests
duration: 60, // Per minute
});
const rateLimitPlugin = {
requestDidStart() {
return {
async willSendRequest(requestContext) {
const { context } = requestContext;
const key = context.userId || context.ip;
try {
await rateLimiter.consume(key);
} catch (rejRes) {
throw new Error('Too many requests');
}
},
};
},
};
Testing Federation
1. Integration Testing
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { createTestClient } from 'apollo-server-testing';
describe('User Service', () => {
let server;
let query, mutate;
beforeAll(async () => {
server = new ApolloServer({
schema: buildSubgraphSchema([{ typeDefs, resolvers }]),
});
const testClient = createTestClient(server);
query = testClient.query;
mutate = testClient.mutate;
});
test('should fetch user by id', async () => {
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
username
email
}
}
`;
const { data } = await query({
query: GET_USER,
variables: { id: '1' },
});
expect(data.user).toMatchObject({
id: '1',
username: 'alice',
email: '[email protected]',
});
});
});
2. Schema Validation
// Validate composed schema
import { composeAndValidate } from '@apollo/federation';
const serviceList = [
{
name: 'users',
typeDefs: usersTypeDefs,
},
{
name: 'posts',
typeDefs: postsTypeDefs,
},
];
const { errors, supergraphSdl } = composeAndValidate(serviceList);
if (errors && errors.length > 0) {
throw new Error(errors.map(e => e.message).join('\n'));
}
Production Deployment
1. Docker Compose Setup
# docker-compose.yml
version: '3.8'
services:
gateway:
build: ./gateway
ports:
- "4000:4000"
environment:
- NODE_ENV=production
- APOLLO_KEY=${APOLLO_KEY}
- APOLLO_GRAPH_REF=${APOLLO_GRAPH_REF}
depends_on:
- users
- posts
- comments
users:
build: ./services/users
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/users
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
posts:
build: ./services/posts
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/posts
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
postgres:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
2. Kubernetes Deployment
# gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: apollo-gateway
spec:
replicas: 3
selector:
matchLabels:
app: apollo-gateway
template:
metadata:
labels:
app: apollo-gateway
spec:
containers:
- name: gateway
image: myregistry/apollo-gateway:latest
ports:
- containerPort: 4000
env:
- name: APOLLO_KEY
valueFrom:
secretKeyRef:
name: apollo-secrets
key: apollo-key
- name: SERVICE_LIST
value: |
[
{"name": "users", "url": "http://users-service:4001/graphql"},
{"name": "posts", "url": "http://posts-service:4002/graphql"}
]
livenessProbe:
httpGet:
path: /.well-known/apollo/server-health
port: 4000
initialDelaySeconds: 30
periodSeconds: 10
Monitoring and Observability
1. Apollo Studio Integration
// Enhanced gateway with Apollo Studio
import { ApolloServer } from '@apollo/server';
import { ApolloGateway } from '@apollo/gateway';
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';
const server = new ApolloServer({
gateway,
plugins: [
ApolloServerPluginUsageReporting({
sendVariableValues: { all: true },
sendHeaders: { all: true },
generateClientInfo: ({ request }) => {
const headers = request.http.headers;
return {
clientName: headers.get('apollo-client-name'),
clientVersion: headers.get('apollo-client-version'),
};
},
}),
],
});
2. Custom Metrics with Prometheus
import { register, Counter, Histogram } from 'prom-client';
const queryCounter = new Counter({
name: 'graphql_queries_total',
help: 'Total number of GraphQL queries',
labelNames: ['operation', 'service'],
});
const queryDuration = new Histogram({
name: 'graphql_query_duration_seconds',
help: 'GraphQL query duration in seconds',
labelNames: ['operation', 'service'],
buckets: [0.1, 0.5, 1, 2, 5],
});
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Best Practices
- Service Boundaries: Define clear boundaries between services based on business domains
- Shared Nothing: Avoid sharing databases between services
- Versioning: Use field deprecation instead of versioning entire services
- Error Handling: Implement consistent error formatting across services
- Testing: Test both individual services and the composed schema
- Documentation: Keep schemas well-documented with descriptions
- Performance: Monitor and optimize resolver performance continuously
Conclusion
GraphQL Federation provides a powerful approach to building scalable, maintainable GraphQL APIs in a microservices architecture. By allowing teams to work independently while maintaining a unified graph, it solves many of the challenges associated with distributed systems.
The key to success with Federation is thoughtful service design, robust testing, and comprehensive monitoring. As your system grows, Federation's benefits become increasingly apparent, enabling you to scale both your technology and your teams effectively.