GraphQL Federation on Kubernetes: Enterprise API Gateway Architecture
GraphQL federation enables enterprises to build unified API gateways across distributed microservices while maintaining team autonomy. This comprehensive guide covers implementing production-grade GraphQL federation on Kubernetes, including schema design, gateway deployment, performance optimization, and operational best practices.
GraphQL Federation on Kubernetes: Enterprise API Gateway Architecture
Executive Summary
GraphQL federation provides a declarative approach to composing microservice APIs into a unified graph. By deploying federated GraphQL on Kubernetes, enterprises gain scalable, resilient API gateways that enable independent service development while providing clients with a single, cohesive API surface.
Understanding GraphQL Federation
Federation Architecture Overview
Federated GraphQL Architecture:
# graphql-federation-architecture.yaml
apiVersion: architecture.graphql.io/v1
kind: FederationArchitecture
metadata:
name: enterprise-graphql-federation
spec:
components:
gateway:
name: "Apollo Gateway"
role: "Query Planning and Execution"
responsibilities:
- "Schema composition from subgraphs"
- "Query planning and optimization"
- "Request routing to subgraphs"
- "Response aggregation"
- "Caching and performance optimization"
subgraphs:
- name: "Users Service"
domain: "Identity and Authentication"
entities:
- User
- Account
- Permission
endpoints:
- path: /graphql
port: 4001
- name: "Products Service"
domain: "Product Catalog"
entities:
- Product
- Category
- Inventory
endpoints:
- path: /graphql
port: 4002
- name: "Orders Service"
domain: "Order Management"
entities:
- Order
- OrderItem
- Payment
endpoints:
- path: /graphql
port: 4003
- name: "Reviews Service"
domain: "User Generated Content"
entities:
- Review
- Rating
- Comment
endpoints:
- path: /graphql
port: 4004
federationPrinciples:
separation:
- "Each subgraph owns its domain entities"
- "Services extend entities owned by others"
- "No circular dependencies between subgraphs"
composition:
- "Gateway composes schema from all subgraphs"
- "Entities can be referenced across subgraphs"
- "Type extensions enable cross-service relationships"
execution:
- "Gateway plans optimal query execution"
- "Parallel requests where possible"
- "Efficient entity resolution"
dataFlow:
clientRequest:
- "Client sends query to gateway"
- "Gateway validates against composed schema"
- "Gateway creates query plan"
queryExecution:
- "Gateway executes plan across subgraphs"
- "Subgraphs resolve their portions"
- "Gateway aggregates responses"
response:
- "Gateway merges subgraph responses"
- "Gateway applies caching policies"
- "Gateway returns unified response"
scalability:
horizontal:
- "Gateway: Multiple replicas behind load balancer"
- "Subgraphs: Independent scaling per service"
- "Caching: Distributed cache layer"
vertical:
- "Query complexity limits"
- "Depth limits"
- "Rate limiting per client"
Federation Schema Design
Federated Schema Patterns:
# users-service-schema.graphql
# Users subgraph - owns User entity
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable", "@external", "@requires", "@provides"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
createdAt: DateTime!
# This field is provided to other services
verified: Boolean!
# Metadata for other services
metadata: UserMetadata
}
type UserMetadata @shareable {
joinDate: DateTime!
lastLogin: DateTime
tier: UserTier!
}
enum UserTier {
FREE
PREMIUM
ENTERPRISE
}
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
me: User
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
email: String
name: String
}
# products-service-schema.graphql
# Products subgraph - owns Product entity, extends User
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Product @key(fields: "id") {
id: ID!
name: String!
description: String
price: Money!
inventory: Int!
category: Category
# Reference to User who created the product
createdBy: User
createdAt: DateTime!
}
type Money {
amount: Float!
currency: String!
}
type Category @key(fields: "id") {
id: ID!
name: String!
slug: String!
products(limit: Int = 20): [Product!]!
}
# Extend User entity from users service
extend type User @key(fields: "id") {
id: ID! @external
# Add products relationship
products: [Product!]!
# Add favorite products
favoriteProducts: [Product!]!
}
type Query {
product(id: ID!): Product
products(
categoryId: ID
limit: Int = 20
offset: Int = 0
): ProductConnection!
searchProducts(query: String!, limit: Int = 20): [Product!]!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProductEdge {
node: Product!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Mutation {
createProduct(input: CreateProductInput!): Product!
updateProduct(id: ID!, input: UpdateProductInput!): Product!
deleteProduct(id: ID!): Boolean!
}
input CreateProductInput {
name: String!
description: String
price: MoneyInput!
inventory: Int!
categoryId: ID!
}
input MoneyInput {
amount: Float!
currency: String!
}
input UpdateProductInput {
name: String
description: String
price: MoneyInput
inventory: Int
categoryId: ID
}
# orders-service-schema.graphql
# Orders subgraph - owns Order entity, references User and Product
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Order @key(fields: "id") {
id: ID!
orderNumber: String!
customer: User!
items: [OrderItem!]!
total: Money!
status: OrderStatus!
createdAt: DateTime!
updatedAt: DateTime!
# Computed field requiring external data
estimatedDelivery: DateTime @requires(fields: "customer { metadata { tier } }")
}
type OrderItem {
id: ID!
product: Product!
quantity: Int!
price: Money!
subtotal: Money!
}
enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
# Extend User to add orders relationship
extend type User @key(fields: "id") {
id: ID! @external
metadata: UserMetadata @external
orders(limit: Int = 10): [Order!]!
orderCount: Int!
}
type UserMetadata {
tier: UserTier @external
}
enum UserTier {
FREE
PREMIUM
ENTERPRISE
}
# Extend Product to reference from orders
extend type Product @key(fields: "id") {
id: ID! @external
orderCount: Int!
}
type Query {
order(id: ID!): Order
orders(
customerId: ID
status: OrderStatus
limit: Int = 20
): [Order!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
cancelOrder(id: ID!): Order!
}
input CreateOrderInput {
customerId: ID!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
# reviews-service-schema.graphql
# Reviews subgraph - owns Review entity, extends User and Product
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external", "@requires"])
type Review @key(fields: "id") {
id: ID!
product: Product!
author: User!
rating: Int!
title: String
content: String!
helpful: Int!
verified: Boolean!
createdAt: DateTime!
}
# Extend Product to add reviews
extend type Product @key(fields: "id") {
id: ID! @external
reviews(limit: Int = 10): [Review!]!
averageRating: Float
reviewCount: Int!
}
# Extend User to add reviews
extend type User @key(fields: "id") {
id: ID! @external
verified: Boolean! @external
reviews(limit: Int = 10): [Review!]!
reviewCount: Int!
}
type Query {
review(id: ID!): Review
reviews(
productId: ID
authorId: ID
minRating: Int
limit: Int = 20
): [Review!]!
}
type Mutation {
createReview(input: CreateReviewInput!): Review!
updateReview(id: ID!, input: UpdateReviewInput!): Review!
deleteReview(id: ID!): Boolean!
markReviewHelpful(id: ID!): Review!
}
input CreateReviewInput {
productId: ID!
rating: Int!
title: String
content: String!
}
input UpdateReviewInput {
rating: Int
title: String
content: String
}
Apollo Gateway Deployment on Kubernetes
Gateway Configuration and Deployment
Apollo Gateway Implementation:
// gateway.js
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
const { ApolloServer } = require('apollo-server-express');
const express = require('express');
const { createPrometheusExporterPlugin } = require('@bmatei/apollo-prometheus-exporter');
const { ApolloServerPluginLandingPageGraphQLPlayground } = require('apollo-server-core');
// Subgraph service list
const serviceList = [
{ name: 'users', url: process.env.USERS_SERVICE_URL || 'http://users-service:4001/graphql' },
{ name: 'products', url: process.env.PRODUCTS_SERVICE_URL || 'http://products-service:4002/graphql' },
{ name: 'orders', url: process.env.ORDERS_SERVICE_URL || 'http://orders-service:4003/graphql' },
{ name: 'reviews', url: process.env.REVIEWS_SERVICE_URL || 'http://reviews-service:4004/graphql' },
];
// Create gateway with supergraph composition
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: serviceList,
// Poll for schema updates
pollIntervalInMs: parseInt(process.env.SCHEMA_POLL_INTERVAL || '30000'),
}),
// Query plan caching
experimental_approximateQueryPlanStoreMiB: parseInt(process.env.QUERY_PLAN_CACHE_SIZE || '30'),
// Service health checks
serviceHealthCheck: true,
buildService({ url }) {
const { RemoteGraphQLDataSource } = require('@apollo/gateway');
return new RemoteGraphQLDataSource({
url,
// Add authentication headers
willSendRequest({ request, context }) {
if (context.authToken) {
request.http.headers.set('authorization', context.authToken);
}
// Add request ID for tracing
request.http.headers.set('x-request-id', context.requestId);
},
// Handle errors
didReceiveResponse({ response, request, context }) {
console.log(`Subgraph response from ${url}: ${response.status}`);
return response;
},
});
},
});
// Create Apollo Server
const server = new ApolloServer({
gateway,
// Context function
context: ({ req }) => ({
authToken: req.headers.authorization,
requestId: req.headers['x-request-id'] || generateRequestId(),
user: extractUserFromToken(req.headers.authorization),
}),
// Plugins
plugins: [
// Prometheus metrics
createPrometheusExporterPlugin({ app: express(), path: '/metrics' }),
// GraphQL Playground
ApolloServerPluginLandingPageGraphQLPlayground({
settings: {
'request.credentials': 'include',
},
}),
// Custom logging plugin
{
async requestDidStart(requestContext) {
console.log(`Request started: ${requestContext.request.operationName}`);
const start = Date.now();
return {
async willSendResponse(requestContext) {
const duration = Date.now() - start;
console.log(
`Request completed: ${requestContext.request.operationName} (${duration}ms)`
);
},
async didEncounterErrors(requestContext) {
console.error(
`Request error: ${requestContext.request.operationName}`,
requestContext.errors
);
},
};
},
},
],
// Introspection and playground
introspection: process.env.ENABLE_INTROSPECTION === 'true',
// Performance settings
persistedQueries: {
cache: 'bounded',
},
formatError: (error) => {
console.error('GraphQL Error:', error);
// Don't expose internal errors to clients
if (error.message.startsWith('INTERNAL')) {
return new Error('Internal server error');
}
return error;
},
});
// Express app
const app = express();
// Health check endpoints
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
app.get('/ready', async (req, res) => {
try {
// Check gateway and subgraph connectivity
const health = await gateway.__testing().serviceHealthCheck?.();
if (health) {
res.status(200).json({ status: 'ready', subgraphs: health });
} else {
res.status(503).json({ status: 'not ready' });
}
} catch (error) {
res.status(503).json({ status: 'not ready', error: error.message });
}
});
// Start server
async function startServer() {
await server.start();
server.applyMiddleware({ app, path: '/graphql' });
const port = process.env.PORT || 4000;
app.listen(port, () => {
console.log(`🚀 Gateway ready at http://localhost:${port}${server.graphqlPath}`);
console.log(`🏥 Health check at http://localhost:${port}/health`);
console.log(`📊 Metrics at http://localhost:${port}/metrics`);
});
}
startServer().catch((error) => {
console.error('Failed to start gateway:', error);
process.exit(1);
});
// Helper functions
function generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
function extractUserFromToken(authHeader) {
// Implement JWT token validation and user extraction
if (!authHeader) return null;
try {
const token = authHeader.replace('Bearer ', '');
// Verify and decode JWT token
// return decoded user information
return null;
} catch (error) {
console.error('Token validation error:', error);
return null;
}
}
Kubernetes Deployment Manifests:
# graphql-gateway-deployment.yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: graphql
---
# Apollo Gateway Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: apollo-gateway
namespace: graphql
labels:
app: apollo-gateway
spec:
replicas: 3
selector:
matchLabels:
app: apollo-gateway
template:
metadata:
labels:
app: apollo-gateway
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "4000"
prometheus.io/path: "/metrics"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- apollo-gateway
topologyKey: kubernetes.io/hostname
containers:
- name: gateway
image: company/apollo-gateway:1.0.0
ports:
- name: http
containerPort: 4000
protocol: TCP
env:
- name: PORT
value: "4000"
- name: NODE_ENV
value: "production"
- name: ENABLE_INTROSPECTION
value: "false"
- name: SCHEMA_POLL_INTERVAL
value: "30000"
- name: QUERY_PLAN_CACHE_SIZE
value: "50"
# Subgraph URLs
- name: USERS_SERVICE_URL
value: "http://users-service.graphql.svc.cluster.local:4001/graphql"
- name: PRODUCTS_SERVICE_URL
value: "http://products-service.graphql.svc.cluster.local:4002/graphql"
- name: ORDERS_SERVICE_URL
value: "http://orders-service.graphql.svc.cluster.local:4003/graphql"
- name: REVIEWS_SERVICE_URL
value: "http://reviews-service.graphql.svc.cluster.local:4004/graphql"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
livenessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 4000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: apollo-gateway
namespace: graphql
labels:
app: apollo-gateway
spec:
type: ClusterIP
selector:
app: apollo-gateway
ports:
- name: http
port: 80
targetPort: 4000
protocol: TCP
---
# Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: apollo-gateway-hpa
namespace: graphql
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: apollo-gateway
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: apollo-gateway-ingress
namespace: graphql
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: graphql-gateway-tls
rules:
- host: api.example.com
http:
paths:
- path: /graphql
pathType: Prefix
backend:
service:
name: apollo-gateway
port:
number: 80
---
# Example Subgraph Service (Users)
apiVersion: apps/v1
kind: Deployment
metadata:
name: users-service
namespace: graphql
spec:
replicas: 3
selector:
matchLabels:
app: users-service
template:
metadata:
labels:
app: users-service
spec:
containers:
- name: users
image: company/users-service:1.0.0
ports:
- containerPort: 4001
env:
- name: PORT
value: "4001"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: users-db-credentials
key: url
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: users-service
namespace: graphql
spec:
selector:
app: users-service
ports:
- port: 4001
targetPort: 4001
Performance Optimization
Query Complexity and Depth Limiting
Query Complexity Analysis:
// query-complexity.js
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const depthLimit = require('graphql-depth-limit');
// Configure query complexity limits
const complexityLimitRule = createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log(`Query cost: ${cost}`);
},
createError: (cost, max) => {
return new Error(
`Query complexity of ${cost} exceeds maximum allowed complexity of ${max}`
);
},
// Custom field costs
scalarCost: 1,
objectCost: 2,
listFactor: 10,
formatErrorMessage: (cost) => {
return `Query cost (${cost}) exceeds maximum allowed complexity`;
},
});
// Configure depth limit
const depthLimitRule = depthLimit(10, {
ignore: ['__typename', '_service', '_entities'],
});
// Apply to Apollo Server
const server = new ApolloServer({
gateway,
validationRules: [complexityLimitRule, depthLimitRule],
});
// Custom complexity calculation
function calculateQueryComplexity(query, variables) {
const complexityVisitor = {
Field(node) {
const fieldName = node.name.value;
const args = node.arguments || [];
let cost = 1;
// Higher cost for list fields with pagination
if (node.selectionSet) {
const limitArg = args.find(arg => arg.name.value === 'limit');
if (limitArg) {
const limit = parseInt(limitArg.value.value);
cost *= Math.min(limit, 100);
}
}
// Higher cost for expensive computed fields
if (['reviews', 'orders', 'products'].includes(fieldName)) {
cost *= 5;
}
return cost;
},
};
return visit(query, complexityVisitor);
}
Response Caching Strategy
Apollo Server Caching:
// caching.js
const { InMemoryLRUCache } = require('@apollo/utils.keyvaluecache');
const { RedisCache } = require('apollo-server-cache-redis');
const responseCachePlugin = require('apollo-server-plugin-response-cache');
// Create Redis cache
const cache = new RedisCache({
host: process.env.REDIS_HOST || 'redis',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
db: process.env.REDIS_DB || 0,
// TTL in seconds
ttl: 300,
});
// Configure response caching
const server = new ApolloServer({
gateway,
cache,
plugins: [
responseCachePlugin({
// Cache based on user session
sessionId: (requestContext) => {
return requestContext.context.user?.id || 'anonymous';
},
// Custom cache key generation
generateCacheKey: (requestContext) => {
const { request, context } = requestContext;
return `${request.operationName}:${context.user?.id}:${JSON.stringify(request.variables)}`;
},
// Don't cache mutations
shouldReadFromCache: (requestContext) => {
return requestContext.request.http?.method === 'GET';
},
shouldWriteToCache: (requestContext) => {
const { operation } = requestContext;
return operation?.operation === 'query';
},
}),
],
});
// Schema-level cache control
const typeDefs = gql`
type Product @cacheControl(maxAge: 600) {
id: ID!
name: String!
price: Money! @cacheControl(maxAge: 300)
}
type User @cacheControl(maxAge: 60) {
id: ID!
email: String!
orders: [Order!]! @cacheControl(maxAge: 30)
}
`;
Monitoring and Observability
GraphQL Metrics and Tracing:
# graphql-monitoring.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-graphql-dashboard
namespace: monitoring
data:
graphql-dashboard.json: |
{
"dashboard": {
"title": "GraphQL Federation Metrics",
"panels": [
{
"title": "Request Rate",
"targets": [{
"expr": "rate(graphql_request_total[5m])"
}]
},
{
"title": "Request Duration (p95)",
"targets": [{
"expr": "histogram_quantile(0.95, rate(graphql_request_duration_seconds_bucket[5m]))"
}]
},
{
"title": "Error Rate",
"targets": [{
"expr": "rate(graphql_request_errors_total[5m])"
}]
},
{
"title": "Query Complexity",
"targets": [{
"expr": "avg(graphql_query_complexity)"
}]
},
{
"title": "Subgraph Latency",
"targets": [{
"expr": "avg by (subgraph) (graphql_subgraph_request_duration_seconds)"
}]
},
{
"title": "Cache Hit Rate",
"targets": [{
"expr": "rate(graphql_cache_hits_total[5m]) / rate(graphql_cache_requests_total[5m])"
}]
}
]
}
}
Conclusion
GraphQL federation on Kubernetes enables enterprises to:
- Unified API Surface: Single GraphQL endpoint for all services
- Team Autonomy: Independent service development and deployment
- Scalability: Horizontal scaling of gateway and subgraphs
- Performance: Query planning, caching, and optimization
- Flexibility: Easy addition of new services and features
- Type Safety: Strong typing across the entire graph
By implementing federated GraphQL with the patterns in this guide, organizations can build scalable, maintainable API architectures that evolve with business needs.
For more information on GraphQL and API architecture, visit support.tools.