Microservices architecture has revolutionized how we build and scale applications. This guide explores essential design patterns and best practices for successful microservices implementation.
Core Microservices Principles
- Single Responsibility: Each service handles one business capability
- Decentralized: Services manage their own data and business logic
- Fault Tolerant: Failures in one service don't cascade
- Technology Agnostic: Services can use different technologies
Service Design Patterns
API Gateway Pattern
Centralize cross-cutting concerns like authentication and routing:
// Express.js API Gateway
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const app = express();
// Authentication middleware
app.use('/api', authenticateToken);
// Route to user service
app.use('/api/users', httpProxy({
target: 'http://user-service:3001',
changeOrigin: true
}));
// Route to order service
app.use('/api/orders', httpProxy({
target: 'http://order-service:3002',
changeOrigin: true
}));
Circuit Breaker Pattern
Prevent cascading failures:
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = 'CLOSED';
this.failureCount = 0;
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000;
this.monitor = options.monitor || console.log;
}
async call(...args) {
if (this.state === 'OPEN') {
if (this.nextAttempt <= Date.now()) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await this.request(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
}
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
Data Management Patterns
Database per Service
Each microservice owns its data:
// User Service - PostgreSQL
const userDb = new Pool({
connectionString: process.env.USER_DB_URL
});
// Order Service - MongoDB
const orderDb = mongoose.connect(process.env.ORDER_DB_URL);
// Analytics Service - Redis
const analyticsDb = redis.createClient({
url: process.env.REDIS_URL
});
Saga Pattern for Distributed Transactions
class OrderSaga {
async execute(orderData) {
const steps = [
() => this.reserveInventory(orderData),
() => this.processPayment(orderData),
() => this.createShipment(orderData),
() => this.sendConfirmation(orderData)
];
const compensations = [
() => this.releaseInventory(orderData),
() => this.refundPayment(orderData),
() => this.cancelShipment(orderData),
() => this.sendCancellation(orderData)
];
try {
for (let i = 0; i < steps.length; i++) {
await steps[i]();
}
return { success: true };
} catch (error) {
// Execute compensating actions
for (let i = this.completedSteps - 1; i >= 0; i--) {
await compensations[i]();
}
throw error;
}
}
}
Communication Patterns
Event-Driven Architecture
// Event publisher
class EventBus {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
}
publish(event, data) {
if (this.subscribers[event]) {
this.subscribers[event].forEach(callback => {
callback(data);
});
}
}
}
// Usage
const eventBus = new EventBus();
// User service publishes events
eventBus.publish('USER_CREATED', { userId: 123, email: 'user@example.com' });
// Email service subscribes to events
eventBus.subscribe('USER_CREATED', async (userData) => {
await sendWelcomeEmail(userData.email);
});
Observability and Monitoring
Distributed Tracing
const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('order-service');
async function processOrder(orderId) {
const span = tracer.startSpan('process-order');
span.setAttributes({
'order.id': orderId,
'service.name': 'order-service'
});
try {
await validateOrder(orderId);
await chargePayment(orderId);
await updateInventory(orderId);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
}
Deployment Strategies
Docker and Kubernetes
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
# Kubernetes deployment
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:latest
ports:
- containerPort: 3000
Successful microservices architecture requires careful planning, proper tooling, and adherence to proven patterns. Start small, evolve gradually, and always prioritize observability.