Microservices Architecture: Design Patterns and Best Practices
Microservices architecture has become the standard for building scalable, maintainable applications. Let’s explore key patterns and practices for successful implementation.
What are Microservices?
Microservices are independently deployable services that work together to form a complete application. Each service:
- Focuses on a single business capability
- Has its own database
- Communicates via APIs
- Can be deployed independently
Monolith vs Microservices
Monolithic Architecture
┌─────────────────────────────┐
│ Single Application │
├─────────────────────────────┤
│ User Module │
│ Product Module │
│ Order Module │
│ Payment Module │
├─────────────────────────────┤
│ Shared Database │
└─────────────────────────────┘
Microservices Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Product │ │ Order │
│ Service │ │ Service │ │ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼─────┐ ┌───▼──────┐ ┌───▼──────┐
│ User DB │ │ Product │ │ Order DB │
└──────────┘ │ DB │ └──────────┘
└──────────┘
Key Design Patterns
1. API Gateway Pattern
Single entry point for all clients:
// API Gateway
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// Route to services
app.use('/api/users', createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true
}));
app.use('/api/products', createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true
}));
app.use('/api/orders', createProxyMiddleware({
target: 'http://order-service:3003',
changeOrigin: true
}));
app.listen(3000);
2. Service Discovery Pattern
Services register and discover each other:
// Using Consul for service discovery
const consul = require('consul')();
// Register service
consul.agent.service.register({
name: 'user-service',
port: 3001,
check: {
http: 'http://localhost:3001/health',
interval: '10s'
}
});
// Discover service
const services = await consul.health.service('user-service');
const serviceUrl = `http://${services[0].Service.Address}:${services[0].Service.Port}`;
3. Circuit Breaker Pattern
Prevent cascading failures:
const CircuitBreaker = require('opossum');
async function callUserService() {
const response = await fetch('http://user-service/api/users');
return response.json();
}
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
const breaker = new CircuitBreaker(callUserService, options);
breaker.fallback(() => ({ users: [] }));
// Use circuit breaker
app.get('/users', async (req, res) => {
try {
const users = await breaker.fire();
res.json(users);
} catch (err) {
res.status(503).json({ error: 'Service unavailable' });
}
});
4. Saga Pattern
Manage distributed transactions:
// Order Saga
class OrderSaga {
async createOrder(orderData) {
try {
// Step 1: Reserve inventory
const inventory = await inventoryService.reserve(orderData.items);
try {
// Step 2: Process payment
const payment = await paymentService.charge(orderData.payment);
try {
// Step 3: Create order
const order = await orderService.create(orderData);
return order;
} catch (err) {
// Compensate: Refund payment
await paymentService.refund(payment.id);
throw err;
}
} catch (err) {
// Compensate: Release inventory
await inventoryService.release(inventory.id);
throw err;
}
} catch (err) {
console.error('Order creation failed:', err);
throw err;
}
}
}
5. Event Sourcing Pattern
Store state as sequence of events:
// Event Store
class EventStore {
async append(aggregateId, event) {
await db.events.insert({
aggregateId,
type: event.type,
data: event.data,
timestamp: new Date()
});
}
async getEvents(aggregateId) {
return db.events.find({ aggregateId }).sort({ timestamp: 1 });
}
}
// Aggregate
class Order {
constructor(id) {
this.id = id;
this.state = 'pending';
this.items = [];
}
apply(event) {
switch (event.type) {
case 'OrderCreated':
this.items = event.data.items;
this.state = 'created';
break;
case 'OrderPaid':
this.state = 'paid';
break;
case 'OrderShipped':
this.state = 'shipped';
break;
}
}
static async load(eventStore, orderId) {
const order = new Order(orderId);
const events = await eventStore.getEvents(orderId);
events.forEach(event => order.apply(event));
return order;
}
}
Communication Patterns
Synchronous Communication (HTTP/gRPC)
// HTTP REST
const response = await fetch('http://user-service/api/users/123');
const user = await response.json();
// gRPC
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition);
const client = new userProto.UserService(
'user-service:50051',
grpc.credentials.createInsecure()
);
client.GetUser({ id: '123' }, (err, user) => {
console.log(user);
});
Asynchronous Communication (Message Queue)
// RabbitMQ Producer
const amqp = require('amqplib');
async function publishEvent(event) {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const exchange = 'events';
await channel.assertExchange(exchange, 'topic', { durable: true });
channel.publish(
exchange,
event.type,
Buffer.from(JSON.stringify(event))
);
await channel.close();
await connection.close();
}
// RabbitMQ Consumer
async function consumeEvents() {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const exchange = 'events';
await channel.assertExchange(exchange, 'topic', { durable: true });
const queue = await channel.assertQueue('', { exclusive: true });
await channel.bindQueue(queue.queue, exchange, 'order.*');
channel.consume(queue.queue, (msg) => {
const event = JSON.parse(msg.content.toString());
handleEvent(event);
channel.ack(msg);
});
}
Data Management
Database per Service
// User Service - PostgreSQL
const userDb = new PostgreSQL({
host: 'user-db',
database: 'users'
});
// Product Service - MongoDB
const productDb = new MongoDB({
host: 'product-db',
database: 'products'
});
// Order Service - PostgreSQL
const orderDb = new PostgreSQL({
host: 'order-db',
database: 'orders'
});
CQRS (Command Query Responsibility Segregation)
// Write Model (Commands)
class OrderCommandHandler {
async createOrder(command) {
const order = new Order(command.data);
await orderWriteDb.save(order);
await eventBus.publish('OrderCreated', order);
}
}
// Read Model (Queries)
class OrderQueryHandler {
async getOrder(query) {
return orderReadDb.findById(query.orderId);
}
async getOrdersByUser(query) {
return orderReadDb.find({ userId: query.userId });
}
}
// Event Handler (Update Read Model)
eventBus.on('OrderCreated', async (order) => {
await orderReadDb.insert({
id: order.id,
userId: order.userId,
total: order.total,
createdAt: order.createdAt
});
});
Observability
Distributed Tracing
const { trace } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service');
app.post('/orders', async (req, res) => {
const span = tracer.startSpan('create-order');
try {
// Call inventory service
const inventorySpan = tracer.startSpan('check-inventory', {
parent: span
});
await inventoryService.check(req.body.items);
inventorySpan.end();
// Call payment service
const paymentSpan = tracer.startSpan('process-payment', {
parent: span
});
await paymentService.charge(req.body.payment);
paymentSpan.end();
const order = await createOrder(req.body);
res.json(order);
} catch (err) {
span.recordException(err);
throw err;
} finally {
span.end();
}
});
Centralized Logging
const winston = require('winston');
const { LogstashTransport } = require('winston-logstash-transport');
const logger = winston.createLogger({
format: winston.format.json(),
defaultMeta: {
service: 'order-service',
version: '1.0.0'
},
transports: [
new LogstashTransport({
host: 'logstash',
port: 5000
})
]
});
logger.info('Order created', {
orderId: order.id,
userId: order.userId,
total: order.total
});
Deployment
Docker Compose
version: '3.8'
services:
api-gateway:
build: ./api-gateway
ports:
- "3000:3000"
depends_on:
- user-service
- product-service
user-service:
build: ./user-service
environment:
- DB_HOST=user-db
depends_on:
- user-db
user-db:
image: postgres:14
environment:
- POSTGRES_DB=users
product-service:
build: ./product-service
environment:
- DB_HOST=product-db
depends_on:
- product-db
product-db:
image: mongo:6
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:1.0
ports:
- containerPort: 3000
env:
- name: DB_HOST
value: user-db
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 3000
Best Practices
- Single Responsibility: Each service does one thing well
- Decoupling: Services should be loosely coupled
- Resilience: Design for failure (circuit breakers, retries)
- Observability: Logging, metrics, tracing
- Automation: CI/CD, infrastructure as code
- API Versioning: Maintain backward compatibility
- Security: Authentication, authorization, encryption
Common Pitfalls
❌ Too many services: Start with a few, split when needed ❌ Shared database: Violates service independence ❌ Synchronous coupling: Use async messaging when possible ❌ No API gateway: Clients shouldn’t call services directly ❌ Ignoring network failures: Always plan for failures
Conclusion
Microservices offer significant benefits but come with complexity. Start simple, focus on clear boundaries, invest in automation and observability, and scale your architecture as your application grows.