Unified API Gateway for Microservices Architecture
A GraphQL Gateway that unifies multiple microservices into a single, cohesive API endpoint. This architecture implements Apollo Federation and Schema Stitching to create a distributed graph that allows clients to query data from multiple services through one unified interface.
Apollo Server + Federation
Port 4001
Port 4002
Port 4003
Port 4004
The gateway implements a federated architecture where each microservice is responsible for its own domain and can be developed, deployed, and scaled independently.
Declarative composition of distributed GraphQL schemas using the @key, @extends, and @external directives for seamless type sharing.
Alternative approach using @graphql-tools/stitch to merge multiple schemas into a unified gateway schema with custom type merging.
Dynamic service registration and health checks using Consul for automatic failover and load balancing across service instances.
Batching and caching layer using DataLoader to prevent N+1 queries when resolving cross-service relationships.
Centralized error aggregation with service-specific error codes, retry logic, and graceful degradation for service failures.
Distributed tracing with Jaeger, metrics collection with Prometheus, and real-time dashboard with Grafana.
Each microservice is a standalone GraphQL API with its own database, business logic, and schema. The gateway federates these services into a unified graph.
Responsibility: User authentication, profiles, and account management
Database: PostgreSQL
Key Types:
User - User profile and authenticationAddress - Shipping and billing addressesPaymentMethod - Saved payment informationOperations: Register, login, updateProfile, changePassword
Responsibility: Product catalog, inventory, and categories
Database: MongoDB
Key Types:
Product - Product details and specificationsCategory - Product categorizationInventory - Stock levels and availabilityOperations: searchProducts, getProduct, updateInventory
Responsibility: Order processing, checkout, and fulfillment
Database: PostgreSQL
Key Types:
Order - Order details and statusOrderItem - Individual items in orderPayment - Payment transactionsOperations: createOrder, updateStatus, processPayment
Responsibility: Product reviews, ratings, and user feedback
Database: MongoDB
Key Types:
Review - Product reviews and ratingsRating - Aggregate product ratingsComment - Review comments and repliesOperations: createReview, updateReview, deleteReview
Schema stitching merges multiple GraphQL schemas into a single unified schema. This approach allows the gateway to present a cohesive API while delegating execution to individual services.
// gateway/server.js
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
const { ApolloServer } = require('apollo-server');
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'products', url: 'http://localhost:4002/graphql' },
{ name: 'orders', url: 'http://localhost:4003/graphql' },
{ name: 'reviews', url: 'http://localhost:4004/graphql' }
],
pollIntervalInMs: 10000 // Poll for schema changes every 10s
}),
debug: true,
serviceHealthCheck: true
});
const server = new ApolloServer({
gateway,
context: ({ req }) => ({
headers: req.headers,
userId: req.headers['x-user-id']
}),
formatError: (error) => {
console.error('Gateway Error:', error);
return {
message: error.message,
code: error.extensions?.code,
service: error.extensions?.serviceName
};
}
});
server.listen(4000).then(({ url }) => {
console.log(`๐ Gateway ready at ${url}`);
});
// users-service/schema.js
const { gql } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
username: String!
firstName: String!
lastName: String!
createdAt: String!
addresses: [Address!]!
paymentMethods: [PaymentMethod!]!
}
type Address {
id: ID!
street: String!
city: String!
state: String!
zipCode: String!
country: String!
isDefault: Boolean!
}
type PaymentMethod {
id: ID!
type: PaymentType!
last4: String!
expiryMonth: Int
expiryYear: Int
isDefault: Boolean!
}
enum PaymentType {
CREDIT_CARD
DEBIT_CARD
PAYPAL
}
extend type Query {
me: User
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
}
extend type Mutation {
register(input: RegisterInput!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
updateProfile(input: UpdateProfileInput!): User!
addAddress(input: AddressInput!): Address!
addPaymentMethod(input: PaymentMethodInput!): PaymentMethod!
}
input RegisterInput {
email: String!
password: String!
username: String!
firstName: String!
lastName: String!
}
input UpdateProfileInput {
username: String
firstName: String
lastName: String
}
input AddressInput {
street: String!
city: String!
state: String!
zipCode: String!
country: String!
isDefault: Boolean
}
input PaymentMethodInput {
type: PaymentType!
cardNumber: String!
expiryMonth: Int!
expiryYear: Int!
cvv: String!
isDefault: Boolean
}
type AuthPayload {
token: String!
user: User!
}
`;
const resolvers = {
User: {
__resolveReference(user, { dataSources }) {
return dataSources.usersAPI.getUserById(user.id);
}
},
Query: {
me: (_, __, { userId, dataSources }) => {
return dataSources.usersAPI.getUserById(userId);
},
user: (_, { id }, { dataSources }) => {
return dataSources.usersAPI.getUserById(id);
},
users: (_, { limit, offset }, { dataSources }) => {
return dataSources.usersAPI.getUsers(limit, offset);
}
}
};
module.exports = buildSubgraphSchema([{ typeDefs, resolvers }]);
// products-service/schema.js
const { gql } = require('apollo-server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const typeDefs = gql`
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: Float!
category: Category!
images: [String!]!
specifications: [Specification!]!
inventory: Inventory!
averageRating: Float # Computed from reviews service
reviewCount: Int # Computed from reviews service
}
type Category @key(fields: "id") {
id: ID!
name: String!
slug: String!
parentCategory: Category
products: [Product!]!
}
type Inventory {
quantity: Int!
status: StockStatus!
lastUpdated: String!
}
type Specification {
key: String!
value: String!
}
enum StockStatus {
IN_STOCK
LOW_STOCK
OUT_OF_STOCK
DISCONTINUED
}
extend type Query {
product(id: ID!): Product
products(
categoryId: ID
search: String
minPrice: Float
maxPrice: Float
limit: Int
offset: Int
): ProductConnection!
category(id: ID!): Category
categories: [Category!]!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProductEdge {
node: Product!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
extend type Mutation {
createProduct(input: CreateProductInput!): Product!
updateProduct(id: ID!, input: UpdateProductInput!): Product!
updateInventory(productId: ID!, quantity: Int!): Inventory!
}
input CreateProductInput {
name: String!
description: String!
price: Float!
categoryId: ID!
images: [String!]!
specifications: [SpecificationInput!]
initialQuantity: Int!
}
input UpdateProductInput {
name: String
description: String
price: Float
categoryId: ID
images: [String!]
specifications: [SpecificationInput!]
}
input SpecificationInput {
key: String!
value: String!
}
`;
const resolvers = {
Product: {
__resolveReference(product, { dataSources }) {
return dataSources.productsAPI.getProductById(product.id);
}
},
Query: {
product: (_, { id }, { dataSources }) => {
return dataSources.productsAPI.getProductById(id);
},
products: (_, args, { dataSources }) => {
return dataSources.productsAPI.getProducts(args);
}
}
};
module.exports = buildSubgraphSchema([{ typeDefs, resolvers }]);
// orders-service/schema.js
const typeDefs = gql`
# Extend User from users-service
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
# Extend Product from products-service
extend type Product @key(fields: "id") {
id: ID! @external
name: String! @external
price: Float! @external
}
type Order @key(fields: "id") {
id: ID!
user: User!
items: [OrderItem!]!
total: Float!
status: OrderStatus!
paymentStatus: PaymentStatus!
shippingAddress: Address!
createdAt: String!
updatedAt: String!
}
type OrderItem {
product: Product!
quantity: Int!
price: Float!
subtotal: Float!
}
enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
enum PaymentStatus {
PENDING
AUTHORIZED
CAPTURED
FAILED
REFUNDED
}
`;
const resolvers = {
User: {
orders: (user, _, { dataSources }) => {
return dataSources.ordersAPI.getOrdersByUserId(user.id);
}
},
Order: {
__resolveReference(order, { dataSources }) {
return dataSources.ordersAPI.getOrderById(order.id);
},
user: (order) => {
return { __typename: 'User', id: order.userId };
}
},
OrderItem: {
product: (orderItem) => {
return { __typename: 'Product', id: orderItem.productId };
},
subtotal: (orderItem) => {
return orderItem.price * orderItem.quantity;
}
}
};
Key implementation patterns for building a robust GraphQL gateway with microservices.
// shared/dataloaders.js
const DataLoader = require('dataloader');
class ServiceDataLoaders {
constructor(services) {
this.services = services;
// User DataLoader
this.userLoader = new DataLoader(async (userIds) => {
const users = await this.services.users.getUsersByIds(userIds);
return userIds.map(id => users.find(u => u.id === id) || null);
}, { cache: true, maxBatchSize: 100 });
// Product DataLoader
this.productLoader = new DataLoader(async (productIds) => {
const products = await this.services.products.getProductsByIds(productIds);
return productIds.map(id => products.find(p => p.id === id) || null);
}, { cache: true, maxBatchSize: 100 });
// Order DataLoader
this.orderLoader = new DataLoader(async (orderIds) => {
const orders = await this.services.orders.getOrdersByIds(orderIds);
return orderIds.map(id => orders.find(o => o.id === id) || null);
}, { cache: true, maxBatchSize: 100 });
// Review DataLoader by Product
this.reviewsByProductLoader = new DataLoader(async (productIds) => {
const reviewsMap = await this.services.reviews.getReviewsByProductIds(productIds);
return productIds.map(id => reviewsMap[id] || []);
}, { cache: true, maxBatchSize: 50 });
}
// Clear all caches
clearAll() {
this.userLoader.clearAll();
this.productLoader.clearAll();
this.orderLoader.clearAll();
this.reviewsByProductLoader.clearAll();
}
}
module.exports = ServiceDataLoaders;
// shared/service-client.js
const axios = require('axios');
const CircuitBreaker = require('opossum');
class ServiceClient {
constructor(serviceName, baseURL, options = {}) {
this.serviceName = serviceName;
this.baseURL = baseURL;
// Create axios instance
this.client = axios.create({
baseURL,
timeout: options.timeout || 5000,
headers: {
'Content-Type': 'application/json'
}
});
// Circuit breaker configuration
const breakerOptions = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
rollingCountTimeout: 10000,
rollingCountBuckets: 10
};
// Wrap requests in circuit breaker
this.breaker = new CircuitBreaker(
async (config) => this.client.request(config),
breakerOptions
);
// Circuit breaker events
this.breaker.on('open', () => {
console.error(`Circuit breaker OPEN for ${serviceName}`);
});
this.breaker.on('halfOpen', () => {
console.warn(`Circuit breaker HALF-OPEN for ${serviceName}`);
});
this.breaker.on('close', () => {
console.info(`Circuit breaker CLOSED for ${serviceName}`);
});
}
async query(query, variables = {}, context = {}) {
try {
const response = await this.breaker.fire({
method: 'POST',
url: '/graphql',
data: { query, variables },
headers: {
'x-user-id': context.userId,
'x-request-id': context.requestId,
...context.headers
}
});
if (response.data.errors) {
throw new ServiceError(
`GraphQL errors from ${this.serviceName}`,
response.data.errors
);
}
return response.data.data;
} catch (error) {
if (error.code === 'EOPENBREAKER') {
throw new ServiceUnavailableError(
`Service ${this.serviceName} is currently unavailable`
);
}
throw error;
}
}
async rest(method, endpoint, data = null, context = {}) {
try {
const response = await this.breaker.fire({
method,
url: endpoint,
data,
headers: {
'x-user-id': context.userId,
'x-request-id': context.requestId,
...context.headers
}
});
return response.data;
} catch (error) {
throw new ServiceError(
`REST error from ${this.serviceName}`,
error
);
}
}
getHealth() {
return {
service: this.serviceName,
circuitBreakerOpen: this.breaker.opened,
stats: this.breaker.stats
};
}
}
class ServiceError extends Error {
constructor(message, originalError) {
super(message);
this.name = 'ServiceError';
this.originalError = originalError;
}
}
class ServiceUnavailableError extends Error {
constructor(message) {
super(message);
this.name = 'ServiceUnavailableError';
}
}
module.exports = { ServiceClient, ServiceError, ServiceUnavailableError };
// gateway/middleware/auth.js
const jwt = require('jsonwebtoken');
const { AuthenticationError, ForbiddenError } = require('apollo-server');
const JWT_SECRET = process.env.JWT_SECRET;
// Verify JWT token
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new AuthenticationError('Invalid or expired token');
}
};
// Extract token from request
const getToken = (req) => {
const authHeader = req.headers.authorization || '';
const token = authHeader.replace('Bearer ', '');
return token || null;
};
// Authentication directive
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const context = args[2];
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
return resolve.apply(this, args);
};
}
}
// Role-based authorization directive
class HasRoleDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { role: requiredRole } = this.args;
field.resolve = async function (...args) {
const context = args[2];
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
if (context.user.role !== requiredRole) {
throw new ForbiddenError(
`Requires ${requiredRole} role`
);
}
return resolve.apply(this, args);
};
}
}
// Context builder for Apollo Server
const buildContext = async ({ req }) => {
const token = getToken(req);
let user = null;
if (token) {
try {
user = verifyToken(token);
} catch (error) {
console.error('Token verification failed:', error.message);
}
}
return {
user,
userId: user?.id,
requestId: req.headers['x-request-id'] || generateRequestId(),
headers: req.headers
};
};
// Usage in schema
const typeDefs = gql`
directive @auth on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION
type Query {
me: User @auth
users: [User!]! @hasRole(role: "ADMIN")
}
type Mutation {
updateProfile(input: UpdateProfileInput!): User! @auth
deleteUser(id: ID!): Boolean! @hasRole(role: "ADMIN")
}
`;
module.exports = {
verifyToken,
getToken,
AuthDirective,
HasRoleDirective,
buildContext
};
Apollo Federation enables a distributed graph architecture where multiple subgraphs contribute types and fields to a unified supergraph.
| Feature | Schema Stitching | Apollo Federation |
|---|---|---|
| Type Ownership | Single service owns each type | Multiple services can extend types |
| Schema Composition | Gateway merges schemas at runtime | Declarative with @key directives |
| Type Resolution | Manual delegation configuration | Automatic via __resolveReference |
| Performance | Additional network overhead | Optimized query planning |
| Learning Curve | Moderate - custom resolvers | Steeper - federation concepts |
| Tooling | graphql-tools ecosystem | Apollo Platform (Studio, Router) |
| Best For | Legacy system integration | New microservices architecture |
// Federation v2 Directives
// @key - Defines entity with unique identifier
type User @key(fields: "id") {
id: ID!
email: String!
}
// @extends - Extends entity from another service (v1 only)
extend type User @key(fields: "id") {
orders: [Order!]!
}
// @external - Marks field owned by another service
extend type Product @key(fields: "id") {
id: ID! @external
name: String! @external
reviews: [Review!]!
}
// @requires - Field depends on other fields
type Product @key(fields: "id") {
id: ID!
price: Float!
tax: Float! @requires(fields: "price")
}
// @provides - Optimization hint for nested fields
type Order {
product: Product! @provides(fields: "name price")
}
// @shareable - Multiple subgraphs can define same field (v2)
type Product @key(fields: "id") {
id: ID!
name: String! @shareable
}
// @override - Replace field implementation from another subgraph (v2)
type User @key(fields: "id") {
id: ID!
email: String! @override(from: "legacy-users")
}
// @inaccessible - Hide field from supergraph (v2)
type User {
id: ID!
internalId: String! @inaccessible
}
// Composite @key for multi-field keys
type OrderItem @key(fields: "orderId productId") {
orderId: ID!
productId: ID!
quantity: Int!
}
# Client Query
query GetUserWithOrders {
user(id: "123") {
id
username
email
orders {
id
total
items {
product {
name
price
}
quantity
}
}
}
}
# Query Plan Generated by Gateway
{
"kind": "QueryPlan",
"node": {
"kind": "Sequence",
"nodes": [
{
"kind": "Fetch",
"serviceName": "users",
"query": "{ user(id: \"123\") { id username email } }"
},
{
"kind": "Parallel",
"nodes": [
{
"kind": "Fetch",
"serviceName": "orders",
"requires": [{ "__typename": "User", "id": "123" }],
"query": "{ _entities(representations: [...]) { ... on User { orders { id total items { productId quantity } } } } }"
}
]
},
{
"kind": "Fetch",
"serviceName": "products",
"requires": [{ "__typename": "Product", "id": [...] }],
"query": "{ _entities(representations: [...]) { ... on Product { name price } } }"
}
]
}
}
# Execution Flow:
# 1. Fetch user from users-service
# 2. Fetch orders from orders-service (parallel)
# 3. Fetch product details from products-service
# 4. Merge results and return to client
Strategies for optimizing gateway performance, reducing latency, and improving scalability.
Redis-based caching with TTL strategies for frequently accessed data, reducing load on microservices.
DataLoader batches multiple requests into single database queries, eliminating N+1 problems.
Limits on query depth and complexity prevent expensive operations from overwhelming services.
// gateway/cache/redis-cache.js
const Redis = require('ioredis');
const { createHash } = require('crypto');
class RedisCache {
constructor() {
this.client = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
this.defaultTTL = 300; // 5 minutes
}
// Generate cache key from query
generateKey(query, variables = {}) {
const hash = createHash('sha256');
hash.update(JSON.stringify({ query, variables }));
return `gql:${hash.digest('hex')}`;
}
// Get cached result
async get(query, variables) {
try {
const key = this.generateKey(query, variables);
const cached = await this.client.get(key);
if (cached) {
console.log(`Cache HIT: ${key}`);
return JSON.parse(cached);
}
console.log(`Cache MISS: ${key}`);
return null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
// Set cache with TTL
async set(query, variables, data, ttl = this.defaultTTL) {
try {
const key = this.generateKey(query, variables);
await this.client.setex(
key,
ttl,
JSON.stringify(data)
);
console.log(`Cache SET: ${key} (TTL: ${ttl}s)`);
} catch (error) {
console.error('Cache set error:', error);
}
}
// Invalidate cache by pattern
async invalidate(pattern) {
try {
const keys = await this.client.keys(`gql:${pattern}*`);
if (keys.length > 0) {
await this.client.del(...keys);
console.log(`Cache INVALIDATE: ${keys.length} keys`);
}
} catch (error) {
console.error('Cache invalidate error:', error);
}
}
// Get cache statistics
async getStats() {
const info = await this.client.info('stats');
return {
hits: this.parseInfo(info, 'keyspace_hits'),
misses: this.parseInfo(info, 'keyspace_misses'),
keys: await this.client.dbsize()
};
}
parseInfo(info, key) {
const match = info.match(new RegExp(`${key}:(\\d+)`));
return match ? parseInt(match[1]) : 0;
}
}
// Apollo Server cache plugin
const responseCachePlugin = (cache) => ({
async requestDidStart() {
return {
async willSendResponse({ request, response, context }) {
// Only cache successful queries
if (
request.operationName &&
!response.errors &&
request.http?.method === 'GET'
) {
await cache.set(
request.query,
request.variables,
response.data,
300 // 5 minutes
);
}
}
};
}
});
module.exports = { RedisCache, responseCachePlugin };
// gateway/plugins/complexity.js
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityLimitPlugin = {
requestDidStart() {
return {
didResolveOperation({ request, document, schema }) {
// Calculate query complexity
const complexity = getComplexity({
schema,
query: document,
variables: request.variables,
estimators: [
// Fields have base cost of 1
simpleEstimator({ defaultComplexity: 1 }),
// List fields multiply by limit/first argument
fieldExtensionsEstimator(),
// Custom complexity for expensive fields
directiveEstimator({
name: 'complexity',
argumentName: 'value'
})
]
});
const maxComplexity = 1000;
if (complexity > maxComplexity) {
throw new Error(
`Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`
);
}
console.log(`Query complexity: ${complexity}/${maxComplexity}`);
}
};
}
};
// Schema with complexity annotations
const typeDefs = gql`
type Query {
# Simple query - complexity: 1
user(id: ID!): User
# List with multiplier - complexity: limit * 1
users(limit: Int = 10): [User!]!
# Expensive operation - complexity: 50
searchProducts(query: String!): [Product!]! @complexity(value: 50)
}
type User {
id: ID!
email: String!
# Nested list - complexity: limit * 5
orders(limit: Int = 10): [Order!]! @complexity(value: 5)
}
`;
// Example complexity calculations:
// Query 1: user(id: "123") { email } = 1
// Query 2: users(limit: 100) { email } = 100
// Query 3: users(limit: 10) { orders(limit: 20) { id } } = 10 + (10 * 20 * 5) = 1010 (REJECTED!)
module.exports = complexityLimitPlugin;
Production-ready deployment with Docker, Kubernetes, and CI/CD pipelines.
# docker-compose.yml
version: '3.8'
services:
# Gateway
gateway:
build:
context: ./gateway
dockerfile: Dockerfile
ports:
- "4000:4000"
environment:
- NODE_ENV=production
- JWT_SECRET=${JWT_SECRET}
- REDIS_HOST=redis
- USERS_SERVICE_URL=http://users-service:4001/graphql
- PRODUCTS_SERVICE_URL=http://products-service:4002/graphql
- ORDERS_SERVICE_URL=http://orders-service:4003/graphql
- REVIEWS_SERVICE_URL=http://reviews-service:4004/graphql
depends_on:
- redis
- users-service
- products-service
- orders-service
- reviews-service
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:4000/.well-known/apollo/server-health"]
interval: 30s
timeout: 10s
retries: 3
# Users Service
users-service:
build:
context: ./services/users
dockerfile: Dockerfile
ports:
- "4001:4001"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/users_db
- REDIS_HOST=redis
depends_on:
- postgres
- redis
# Products Service
products-service:
build:
context: ./services/products
dockerfile: Dockerfile
ports:
- "4002:4002"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/products_db
- REDIS_HOST=redis
depends_on:
- mongo
- redis
# Orders Service
orders-service:
build:
context: ./services/orders
dockerfile: Dockerfile
ports:
- "4003:4003"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/orders_db
- REDIS_HOST=redis
depends_on:
- postgres
- redis
# Reviews Service
reviews-service:
build:
context: ./services/reviews
dockerfile: Dockerfile
ports:
- "4004:4004"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/reviews_db
- REDIS_HOST=redis
depends_on:
- mongo
- redis
# PostgreSQL
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-postgres.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
# MongoDB
mongo:
image: mongo:7
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- mongo_data:/data/db
ports:
- "27017:27017"
# Redis
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
# Monitoring - Prometheus
prometheus:
image: prom/prometheus:latest
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
ports:
- "9090:9090"
# Monitoring - Grafana
grafana:
image: grafana/grafana:latest
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana-dashboards:/etc/grafana/provisioning/dashboards
ports:
- "3000:3000"
depends_on:
- prometheus
volumes:
postgres_data:
mongo_data:
redis_data:
prometheus_data:
grafana_data:
# k8s/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: graphql-gateway
labels:
app: graphql-gateway
spec:
replicas: 3
selector:
matchLabels:
app: graphql-gateway
template:
metadata:
labels:
app: graphql-gateway
spec:
containers:
- name: gateway
image: your-registry/graphql-gateway:latest
ports:
- containerPort: 4000
name: http
env:
- name: NODE_ENV
value: "production"
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: gateway-secrets
key: jwt-secret
- name: REDIS_HOST
value: "redis-service"
- name: USERS_SERVICE_URL
value: "http://users-service:4001/graphql"
- name: PRODUCTS_SERVICE_URL
value: "http://products-service:4002/graphql"
- name: ORDERS_SERVICE_URL
value: "http://orders-service:4003/graphql"
- name: REVIEWS_SERVICE_URL
value: "http://reviews-service:4004/graphql"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /.well-known/apollo/server-health
port: 4000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /.well-known/apollo/server-health
port: 4000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: graphql-gateway
spec:
type: LoadBalancer
selector:
app: graphql-gateway
ports:
- protocol: TCP
port: 80
targetPort: 4000
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gateway-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: graphql-gateway
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Clear service boundaries and well-defined schemas are crucial for maintainable microservices. Each service should own its domain completely.
Implement circuit breakers and graceful degradation. A failing microservice shouldn't bring down the entire gateway.
Distributed tracing is essential. Without proper monitoring, debugging issues across services becomes nearly impossible.
DataLoader and caching are not optional - they're required for production. N+1 queries can kill performance at scale.
Authentication at the gateway, authorization at the service level. Each service should validate permissions independently.
Self-documenting GraphQL schemas are powerful, but add descriptions and examples. Good documentation reduces support burden.
This GraphQL Gateway demonstrates a production-ready microservices architecture using Apollo Federation. The federated approach provides flexibility, scalability, and maintainability for complex distributed systems.
Key benefits include unified API, independent service deployment, type safety across services, and optimized query execution. The architecture supports both schema stitching and Apollo Federation, allowing teams to choose the best approach for their needs.