← Back to Guides

Microservices Architecture Patterns

📖 17 min read | 📅 Updated: January 2025 | 🏷️ Backend & APIs

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:

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.