Microservices Architecture Patterns
Introduction
Microservices architecture breaks down applications into small, independent services that communicate via APIs. This guide covers design patterns, service communication, API gateways, service discovery, and best practices for building scalable distributed systems.
1. Microservices Principles
Core Principles:
✓ Single Responsibility - Each service does one thing well
✓ Autonomy - Services are independently deployable
✓ Decentralization - Data and logic distributed
✓ Failure Isolation - Failures don't cascade
✓ Technology Diversity - Use best tool for each service
✓ API-First - Well-defined contracts
Monolith vs Microservices:
Monolith:
- Single codebase
- Shared database
- Single deployment unit
- Tight coupling
- Scale entire app
Microservices:
- Multiple codebases
- Database per service
- Independent deployment
- Loose coupling
- Scale individual services
2. Service Decomposition
// Example: E-commerce system decomposition
Services:
1. User Service (Authentication, profiles)
2. Product Catalog Service (Products, inventory)
3. Order Service (Order management)
4. Payment Service (Payment processing)
5. Shipping Service (Delivery tracking)
6. Notification Service (Email, SMS)
// User Service
POST /api/users/register
POST /api/users/login
GET /api/users/:id
PATCH /api/users/:id
// Product Catalog Service
GET /api/products
GET /api/products/:id
POST /api/products (admin)
PATCH /api/products/:id/inventory
// Order Service
POST /api/orders
GET /api/orders/:id
GET /api/users/:userId/orders
PATCH /api/orders/:id/status
3. API Gateway Pattern
// API Gateway with Express.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
const app = express();
// Route to User Service
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true,
}));
// Route to Product Service
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true,
}));
// Route to Order Service
app.use('/api/orders', createProxyMiddleware({
target: 'http://order-service:3003',
changeOrigin: true,
}));
// Authentication middleware
app.use(async (req, res, next) => {
const token = req.headers.authorization;
if (token) {
try {
const response = await fetch('http://user-service:3001/api/auth/verify', {
headers: { authorization: token }
});
if (response.ok) {
req.user = await response.json();
}
} catch (error) {
console.error('Auth verification failed:', error);
}
}
next();
});
// Rate limiting
import rateLimit from 'express-rate-limit';
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}));
app.listen(3000);
4. Service Communication
Synchronous (REST/gRPC)
// REST communication between services
import axios from 'axios';
class UserServiceClient {
private baseUrl = process.env.USER_SERVICE_URL;
async getUser(userId: string) {
try {
const response = await axios.get(`${this.baseUrl}/users/${userId}`, {
timeout: 5000,
headers: {
'X-Service-Name': 'order-service'
}
});
return response.data;
} catch (error) {
// Handle service unavailability
if (error.code === 'ECONNREFUSED') {
throw new Error('User service unavailable');
}
throw error;
}
}
}
// Order Service using User Service
router.post('/orders', async (req, res) => {
const { userId, items } = req.body;
// Call User Service to verify user
const userClient = new UserServiceClient();
const user = await userClient.getUser(userId);
// Create order
const order = await Order.create({
userId,
items,
userEmail: user.email
});
res.json(order);
});
Asynchronous (Message Queue)
// Using RabbitMQ for async communication
import amqp from 'amqplib';
class MessageBroker {
private connection: amqp.Connection;
private channel: amqp.Channel;
async connect() {
this.connection = await amqp.connect(process.env.RABBITMQ_URL);
this.channel = await this.connection.createChannel();
}
async publish(queue: string, message: any) {
await this.channel.assertQueue(queue, { durable: true });
this.channel.sendToQueue(
queue,
Buffer.from(JSON.stringify(message)),
{ persistent: true }
);
}
async subscribe(queue: string, callback: (msg: any) => void) {
await this.channel.assertQueue(queue, { durable: true });
this.channel.consume(queue, async (msg) => {
if (msg) {
const content = JSON.parse(msg.content.toString());
await callback(content);
this.channel.ack(msg);
}
});
}
}
// Order Service - Publish event
const broker = new MessageBroker();
await broker.connect();
router.post('/orders', async (req, res) => {
const order = await Order.create(req.body);
// Publish order created event
await broker.publish('order.created', {
orderId: order.id,
userId: order.userId,
total: order.total,
timestamp: new Date()
});
res.json(order);
});
// Payment Service - Subscribe to events
await broker.subscribe('order.created', async (event) => {
console.log('Processing payment for order:', event.orderId);
// Process payment
const payment = await processPayment(event);
// Publish payment result
await broker.publish('payment.completed', {
orderId: event.orderId,
paymentId: payment.id,
status: payment.status
});
});
5. Service Discovery
// Using Consul for service discovery
import Consul from 'consul';
class ServiceRegistry {
private consul: Consul.Consul;
constructor() {
this.consul = new Consul({
host: process.env.CONSUL_HOST || 'localhost',
port: process.env.CONSUL_PORT || '8500'
});
}
// Register service
async register(serviceName: string, port: number) {
const serviceId = `${serviceName}-${process.pid}`;
await this.consul.agent.service.register({
id: serviceId,
name: serviceName,
address: process.env.SERVICE_HOST,
port: port,
check: {
http: `http://${process.env.SERVICE_HOST}:${port}/health`,
interval: '10s',
timeout: '5s'
}
});
console.log(`Service ${serviceName} registered`);
// Deregister on exit
process.on('SIGINT', async () => {
await this.consul.agent.service.deregister(serviceId);
process.exit();
});
}
// Discover service
async discover(serviceName: string) {
const services = await this.consul.health.service({
service: serviceName,
passing: true
});
if (services.length === 0) {
throw new Error(`No healthy instances of ${serviceName}`);
}
// Simple round-robin
const service = services[Math.floor(Math.random() * services.length)];
return `http://${service.Service.Address}:${service.Service.Port}`;
}
}
// Usage in service
const registry = new ServiceRegistry();
await registry.register('order-service', 3003);
// Discover and call another service
const userServiceUrl = await registry.discover('user-service');
const response = await axios.get(`${userServiceUrl}/users/123`);
6. Circuit Breaker Pattern
// Prevent cascading failures
class CircuitBreaker {
private failures = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private nextAttempt = Date.now();
constructor(
private threshold = 5,
private timeout = 60000
) {}
async execute(fn: () => Promise): Promise {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Usage
const breaker = new CircuitBreaker(5, 60000);
async function callUserService(userId: string) {
return await breaker.execute(async () => {
const response = await axios.get(`http://user-service/users/${userId}`);
return response.data;
});
}
try {
const user = await callUserService('123');
} catch (error) {
// Use fallback or cached data
const user = await getUserFromCache('123');
}
7. Saga Pattern
// Distributed transactions with Saga
class OrderSaga {
async createOrder(orderData: any) {
const saga = {
steps: [],
compensations: []
};
try {
// Step 1: Create order
const order = await orderService.create(orderData);
saga.steps.push('order_created');
saga.compensations.push(() => orderService.cancel(order.id));
// Step 2: Reserve inventory
await inventoryService.reserve(order.items);
saga.steps.push('inventory_reserved');
saga.compensations.push(() => inventoryService.release(order.items));
// Step 3: Process payment
const payment = await paymentService.charge(order.total);
saga.steps.push('payment_processed');
saga.compensations.push(() => paymentService.refund(payment.id));
// Step 4: Schedule shipping
await shippingService.schedule(order.id);
saga.steps.push('shipping_scheduled');
return order;
} catch (error) {
// Execute compensations in reverse order
console.error('Saga failed, executing compensations');
for (let i = saga.compensations.length - 1; i >= 0; i--) {
try {
await saga.compensations[i]();
} catch (compError) {
console.error('Compensation failed:', compError);
}
}
throw error;
}
}
}
// Event-driven Saga with message broker
class EventDrivenSaga {
constructor(private broker: MessageBroker) {}
async startOrderSaga(orderData: any) {
// Publish initial event
await this.broker.publish('saga.order.started', {
sagaId: generateId(),
orderData
});
}
// Each service handles its part and publishes next event
setupListeners() {
this.broker.subscribe('saga.order.started', async (event) => {
const order = await orderService.create(event.orderData);
await this.broker.publish('saga.inventory.reserve', {
sagaId: event.sagaId,
orderId: order.id,
items: order.items
});
});
this.broker.subscribe('saga.inventory.reserved', async (event) => {
await this.broker.publish('saga.payment.process', event);
});
// Handle failures
this.broker.subscribe('saga.payment.failed', async (event) => {
await this.broker.publish('saga.inventory.release', event);
await this.broker.publish('saga.order.cancel', event);
});
}
}
8. Database per Service
// Each service has its own database
// User Service - PostgreSQL
datasource db {
provider = "postgresql"
url = env("USER_DB_URL")
}
model User {
id String @id @default(uuid())
email String @unique
name String
}
// Order Service - MongoDB
import mongoose from 'mongoose';
const orderSchema = new mongoose.Schema({
userId: String,
items: [{ productId: String, quantity: Number }],
status: String,
total: Number
});
// Product Service - PostgreSQL with read replicas
const masterPool = new Pool({
host: process.env.DB_MASTER_HOST,
database: 'products'
});
const replicaPool = new Pool({
host: process.env.DB_REPLICA_HOST,
database: 'products'
});
// Write operations use master
async function createProduct(data) {
return await masterPool.query(
'INSERT INTO products (name, price) VALUES ($1, $2)',
[data.name, data.price]
);
}
// Read operations use replica
async function getProduct(id) {
return await replicaPool.query(
'SELECT * FROM products WHERE id = $1',
[id]
);
}
9. Monitoring & Observability
// Distributed tracing with OpenTelemetry
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
const provider = new NodeTracerProvider();
provider.register();
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
// Structured logging
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: {
service: 'order-service',
version: '1.0.0'
},
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
],
});
// Add correlation ID
app.use((req, res, next) => {
req.correlationId = req.headers['x-correlation-id'] || generateId();
res.setHeader('x-correlation-id', req.correlationId);
next();
});
// Log with context
logger.info('Order created', {
correlationId: req.correlationId,
orderId: order.id,
userId: order.userId
});
10. Best Practices
✓ Microservices Checklist:
- Start with a monolith, split when needed
- Design services around business capabilities
- Implement API gateway for client communication
- Use service discovery (Consul, Eureka)
- Implement circuit breakers for resilience
- Use async communication when possible
- Database per service pattern
- Implement distributed tracing
- Use correlation IDs across services
- Implement health checks for all services
- Use containerization (Docker)
- Implement centralized logging
- Use API versioning
- Implement Saga pattern for transactions
- Monitor service dependencies
Conclusion
Microservices architecture enables scalability and flexibility but adds complexity. Success requires careful service design, robust communication patterns, proper monitoring, and resilience mechanisms. Start simple, evolve gradually, and always measure the trade-offs.
💡 Pro Tip: Don't start with microservices. Begin with a well-structured monolith and split into microservices only when you have clear boundaries, team structure to support it, and infrastructure to manage distributed systems complexity.