Skip to main content

Microservices Architecture

As applications grow, monolithic architectures become difficult to maintain and scale. Microservices offer a way to build applications as a collection of small, independent services.

Monolith vs Microservices

AspectMonolithMicroservices
DeploymentAll or nothingIndependent per service
ScalingScale entire appScale specific services
TechnologySingle stackBest tool for each job
Team StructureLarge, coordinated teamSmall, autonomous teams
ComplexityIn the codeIn the infrastructure
DataSingle databaseDatabase per service

When to Use Microservices

Use when:
  • Large team (50+ developers)
  • Different scaling needs per feature
  • Need technology diversity
  • High availability requirements
  • Complex domain with clear boundaries
Don’t use when:
  • Small team or startup MVP
  • Simple CRUD application
  • No clear service boundaries
  • Limited DevOps capabilities

Service Design Principles

Single Responsibility

Each service should do one thing well.
✅ Good Service Boundaries:
- User Service (auth, profiles)
- Order Service (cart, checkout)
- Payment Service (transactions)
- Notification Service (email, SMS, push)
- Inventory Service (stock management)

❌ Poor Boundaries:
- User and Order Service (too coupled)
- Everything Service (just a monolith)

API Gateway Pattern

// gateway/app.js
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');

const app = express();

// Rate limiting
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}));

// Authentication middleware
app.use('/api', authMiddleware);

// Route to services
app.use('/api/users', httpProxy.createProxyMiddleware({
  target: 'http://user-service:3001',
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' }
}));

app.use('/api/orders', httpProxy.createProxyMiddleware({
  target: 'http://order-service:3002',
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' }
}));

app.use('/api/products', httpProxy.createProxyMiddleware({
  target: 'http://product-service:3003',
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' }
}));

app.listen(3000);

Service Communication

Synchronous (HTTP/REST)

// order-service calling user-service
const axios = require('axios');

class UserServiceClient {
  constructor() {
    this.baseURL = process.env.USER_SERVICE_URL || 'http://user-service:3001';
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 5000
    });
  }

  async getUser(userId) {
    try {
      const { data } = await this.client.get(`/users/${userId}`);
      return data;
    } catch (error) {
      if (error.response?.status === 404) {
        return null;
      }
      throw new Error(`User service error: ${error.message}`);
    }
  }

  async validateUser(userId) {
    const user = await this.getUser(userId);
    return user !== null;
  }
}

module.exports = new UserServiceClient();

Circuit Breaker Pattern

const CircuitBreaker = require('opossum');

const options = {
  timeout: 3000,       // Time before timeout
  errorThresholdPercentage: 50, // % of failures before opening
  resetTimeout: 30000  // Time before trying again
};

const breaker = new CircuitBreaker(async (userId) => {
  const response = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
  return response.data;
}, options);

breaker.on('open', () => console.log('Circuit breaker opened'));
breaker.on('halfOpen', () => console.log('Circuit breaker half-open'));
breaker.on('close', () => console.log('Circuit breaker closed'));

// Usage
const getUser = async (userId) => {
  try {
    return await breaker.fire(userId);
  } catch (error) {
    // Return cached data or default
    return { id: userId, name: 'Unknown (service unavailable)' };
  }
};

Asynchronous (Message Queue)

npm install amqplib
// RabbitMQ message producer
const amqp = require('amqplib');

class MessageQueue {
  async connect() {
    this.connection = await amqp.connect(process.env.RABBITMQ_URL);
    this.channel = await this.connection.createChannel();
  }

  async publish(queue, message) {
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.sendToQueue(
      queue,
      Buffer.from(JSON.stringify(message)),
      { persistent: true }
    );
  }

  async subscribe(queue, handler) {
    await this.channel.assertQueue(queue, { durable: true });
    this.channel.consume(queue, async (msg) => {
      try {
        const content = JSON.parse(msg.content.toString());
        await handler(content);
        this.channel.ack(msg);
      } catch (error) {
        console.error('Message handling error:', error);
        this.channel.nack(msg, false, false); // Dead letter
      }
    });
  }
}

// Order service - publish event
const orderCreated = async (order) => {
  await mq.publish('order.created', {
    orderId: order.id,
    userId: order.userId,
    items: order.items,
    total: order.total,
    timestamp: new Date().toISOString()
  });
};

// Notification service - consume event
mq.subscribe('order.created', async (event) => {
  const user = await userService.getUser(event.userId);
  await sendEmail(user.email, 'Order Confirmation', {
    orderId: event.orderId,
    total: event.total
  });
});

// Inventory service - consume event
mq.subscribe('order.created', async (event) => {
  for (const item of event.items) {
    await decreaseStock(item.productId, item.quantity);
  }
});

Event-Driven Architecture

// Event types
const EventTypes = {
  USER_CREATED: 'user.created',
  USER_UPDATED: 'user.updated',
  ORDER_CREATED: 'order.created',
  ORDER_PAID: 'order.paid',
  ORDER_SHIPPED: 'order.shipped',
  PAYMENT_RECEIVED: 'payment.received',
  PAYMENT_FAILED: 'payment.failed'
};

// Event structure
const createEvent = (type, data, metadata = {}) => ({
  id: uuid(),
  type,
  data,
  metadata: {
    timestamp: new Date().toISOString(),
    version: '1.0',
    source: process.env.SERVICE_NAME,
    correlationId: metadata.correlationId || uuid(),
    ...metadata
  }
});

// Publish event
await eventBus.publish(createEvent(EventTypes.ORDER_CREATED, {
  orderId: order.id,
  userId: order.userId,
  items: order.items
}));

Service Discovery

With Docker Compose

# docker-compose.yml
version: '3.8'

services:
  gateway:
    build: ./gateway
    ports:
      - "3000:3000"
    depends_on:
      - user-service
      - order-service
    environment:
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=mongodb://mongo:27017/users
    depends_on:
      - mongo

  order-service:
    build: ./order-service
    environment:
      - DATABASE_URL=postgres://postgres:password@postgres:5432/orders
      - USER_SERVICE_URL=http://user-service:3001
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - postgres
      - rabbitmq

  mongo:
    image: mongo:6

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=orders

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "15672:15672"  # Management UI

With Consul (Production)

const Consul = require('consul');

const consul = new Consul({
  host: process.env.CONSUL_HOST || 'localhost'
});

// Register service
const registerService = async () => {
  await consul.agent.service.register({
    name: 'user-service',
    id: `user-service-${process.env.HOSTNAME}`,
    address: process.env.HOST_IP,
    port: parseInt(process.env.PORT),
    check: {
      http: `http://${process.env.HOST_IP}:${process.env.PORT}/health`,
      interval: '10s'
    }
  });
};

// Discover service
const getServiceUrl = async (serviceName) => {
  const services = await consul.catalog.service.nodes(serviceName);
  if (services.length === 0) {
    throw new Error(`Service ${serviceName} not found`);
  }
  // Simple round-robin
  const service = services[Math.floor(Math.random() * services.length)];
  return `http://${service.ServiceAddress}:${service.ServicePort}`;
};

Data Management

Database Per Service

// User service - MongoDB
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI);

const UserSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  name: String,
  createdAt: { type: Date, default: Date.now }
});

// Order service - PostgreSQL with Prisma
// prisma/schema.prisma
model Order {
  id        Int      @id @default(autoincrement())
  userId    String   // Reference to user in another service
  items     Json
  total     Decimal
  status    OrderStatus
  createdAt DateTime @default(now())
}

// Product service - Redis for fast access
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);

const getProduct = async (id) => {
  const cached = await client.get(`product:${id}`);
  if (cached) return JSON.parse(cached);
  
  const product = await fetchFromDB(id);
  await client.setEx(`product:${id}`, 3600, JSON.stringify(product));
  return product;
};

Saga Pattern for Distributed Transactions

// Choreography-based saga
class OrderSaga {
  constructor(eventBus) {
    this.eventBus = eventBus;
    this.setupListeners();
  }

  setupListeners() {
    // Step 1: Order created -> Reserve inventory
    this.eventBus.subscribe('order.created', async (event) => {
      try {
        await this.reserveInventory(event.data);
        await this.eventBus.publish('inventory.reserved', event.data);
      } catch (error) {
        await this.eventBus.publish('inventory.reservation.failed', event.data);
      }
    });

    // Step 2: Inventory reserved -> Process payment
    this.eventBus.subscribe('inventory.reserved', async (event) => {
      try {
        await this.processPayment(event.data);
        await this.eventBus.publish('payment.completed', event.data);
      } catch (error) {
        // Compensate: release inventory
        await this.releaseInventory(event.data);
        await this.eventBus.publish('payment.failed', event.data);
      }
    });

    // Step 3: Payment completed -> Confirm order
    this.eventBus.subscribe('payment.completed', async (event) => {
      await this.confirmOrder(event.data);
      await this.eventBus.publish('order.confirmed', event.data);
    });

    // Compensation handlers
    this.eventBus.subscribe('payment.failed', async (event) => {
      await this.cancelOrder(event.data);
    });
  }
}

Health Checks and Monitoring

// Health check endpoint
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    service: process.env.SERVICE_NAME,
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    checks: {}
  };

  // Database check
  try {
    await db.query('SELECT 1');
    health.checks.database = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.database = { status: 'unhealthy', error: error.message };
  }

  // Message queue check
  try {
    await mq.checkConnection();
    health.checks.messageQueue = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.messageQueue = { status: 'unhealthy', error: error.message };
  }

  // External service check
  try {
    await userService.ping();
    health.checks.userService = { status: 'healthy' };
  } catch (error) {
    health.status = 'degraded'; // Might still work
    health.checks.userService = { status: 'unhealthy', error: error.message };
  }

  const statusCode = health.status === 'healthy' ? 200 : 503;
  res.status(statusCode).json(health);
});

Distributed Tracing

const { trace } = require('@opentelemetry/api');

// Trace an operation
const tracer = trace.getTracer('order-service');

app.post('/orders', async (req, res) => {
  const span = tracer.startSpan('createOrder');
  
  try {
    span.setAttribute('userId', req.body.userId);
    
    // Create child span for each operation
    const userSpan = tracer.startSpan('validateUser', { parent: span });
    const user = await userService.getUser(req.body.userId);
    userSpan.end();
    
    const orderSpan = tracer.startSpan('saveOrder', { parent: span });
    const order = await Order.create(req.body);
    orderSpan.end();
    
    span.setAttribute('orderId', order.id);
    res.json(order);
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    throw error;
  } finally {
    span.end();
  }
});

Summary

  • Start with a monolith - Extract services when needed
  • Define clear boundaries - One service, one responsibility
  • Use API Gateway - Single entry point for clients
  • Implement circuit breakers - Handle service failures gracefully
  • Prefer async communication - Message queues reduce coupling
  • Database per service - Avoid shared databases
  • Handle distributed transactions - Use saga pattern
  • Monitor everything - Health checks, tracing, logging
  • Container orchestration - Use Kubernetes in production