GraphQL Federation: Building Scalable APIs Across Microservices
on Graphql, Microservices, Api, Architecture, Federation
GraphQL Federation: Building Scalable APIs Across Microservices
As organizations adopt microservices architectures, managing APIs becomes increasingly complex. GraphQL Federation offers an elegant solution by allowing teams to build a unified graph while maintaining service autonomy.
Photo by Markus Spiske on Unsplash
What is GraphQL Federation?
GraphQL Federation is an architecture pattern that allows you to compose multiple GraphQL services into a single, unified API. Each service owns a portion of the schema and can be developed, deployed, and scaled independently.
Key Concepts
- Subgraphs: Individual GraphQL services that own specific types
- Supergraph: The composed schema from all subgraphs
- Router/Gateway: The entry point that orchestrates queries across subgraphs
Setting Up Federation 2.0
Let’s build a practical example with three services: Users, Products, and Reviews.
User Service Subgraph
# users/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users: [User!]!
}
// users/resolvers.ts
const resolvers = {
Query: {
user: (_, { id }) => userService.findById(id),
users: () => userService.findAll(),
},
User: {
__resolveReference: (user) => userService.findById(user.id),
},
};
Product Service Subgraph
# products/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key"])
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
description: String
inStock: Boolean!
}
type Query {
product(id: ID!): Product
products(category: String): [Product!]!
}
Reviews Service with Entity Extension
Here’s where Federation shines—extending types from other services:
# reviews/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external"])
type Review @key(fields: "id") {
id: ID!
rating: Int!
comment: String
author: User!
product: Product!
}
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float
}
Photo by NASA on Unsplash
Router Configuration
Using Apollo Router for production:
# router.yaml
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://users-service:4001/graphql
products:
routing_url: http://products-service:4002/graphql
reviews:
routing_url: http://reviews-service:4003/graphql
cors:
origins:
- https://myapp.com
telemetry:
tracing:
otlp:
endpoint: http://jaeger:4317
Query Planning and Execution
When a client sends a query that spans multiple services:
query GetProductWithReviews($productId: ID!) {
product(id: $productId) {
name
price
reviews {
rating
comment
author {
name
}
}
averageRating
}
}
The router automatically:
- Fetches the product from the Products service
- Fetches reviews from the Reviews service
- Fetches user data from the Users service
- Combines the results
Performance Optimization
Batching with DataLoader
// reviews/dataloaders.ts
import DataLoader from 'dataloader';
export const createUserLoader = () =>
new DataLoader<string, User>(async (userIds) => {
const users = await userService.findByIds(userIds);
return userIds.map(id => users.find(u => u.id === id));
});
Query Deduplication
# router.yaml
traffic_shaping:
all:
deduplicate_query: true
subgraphs:
products:
timeout: 5s
Error Handling
Implement partial results for resilience:
type Query {
product(id: ID!): ProductResult!
}
union ProductResult = Product | ProductNotFound | ProductError
type ProductNotFound {
message: String!
}
type ProductError {
code: String!
message: String!
}
Schema Evolution and Versioning
Federation enables safe schema evolution:
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float! @deprecated(reason: "Use priceInfo instead")
priceInfo: PriceInfo!
}
type PriceInfo {
amount: Float!
currency: String!
discount: Float
}
Monitoring and Observability
Essential metrics to track:
- Query latency by subgraph
- Error rates per service
- Cache hit rates
- Query complexity scores
// Custom plugin for metrics
const metricsPlugin = {
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ response }) {
const duration = Date.now() - start;
metrics.histogram('graphql_request_duration', duration);
},
};
},
};
Best Practices
1. Design for Independence
Each subgraph should be deployable independently without breaking the supergraph.
2. Use Semantic Naming
# Good
type Order @key(fields: "id") { ... }
extend type User @key(fields: "id") {
orders: [Order!]!
}
# Avoid
extend type User @key(fields: "id") {
userOrders: [Order!]! # Redundant prefix
}
3. Implement Health Checks
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
schema: 'loaded',
uptime: process.uptime(),
});
});
Conclusion
GraphQL Federation transforms how we build and maintain APIs in a microservices world. By composing independent subgraphs into a unified supergraph, teams can:
- Scale independently: Each team owns their subgraph
- Evolve safely: Schema changes are isolated
- Deliver faster: Single endpoint for all data needs
Start small with a few services and expand as your federation expertise grows.
Have you implemented GraphQL Federation? What challenges did you face? Share in the comments!
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
