Back to Blog

Microservices Architecture: Design Patterns and Best Practices

Backend Team
November 8, 2024
9 min read
Backend Development

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

  1. Single Responsibility: Each service does one thing well
  2. Decoupling: Services should be loosely coupled
  3. Resilience: Design for failure (circuit breakers, retries)
  4. Observability: Logging, metrics, tracing
  5. Automation: CI/CD, infrastructure as code
  6. API Versioning: Maintain backward compatibility
  7. 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.

MicroservicesArchitectureDistributed SystemsBackendScalability

Related Articles

Backend Development

GraphQL vs REST: Choosing the Right API Architecture

Compare GraphQL and REST APIs to understand their strengths, weaknesses, and use cases for making informed architectural decisions...

January 30, 2025
6 min
Read More
Cloud & DevOps

Building Cloud-Native Applications: Best Practices for 2025

Explore modern cloud-native architecture patterns and best practices for building scalable, resilient applications in the cloud...

August 15, 2025
8 min
Read More
Backend Development

Mastering Async Programming in Python with asyncio

Deep dive into Python's asyncio library and learn how to write efficient asynchronous code for high-performance applications...

April 18, 2025
8 min
Read More