Microservices Architecture Patterns

Your Name
March 10, 2025
microservices
architecture
patterns
distributed-systems

Essential patterns and practices for building robust microservices architectures with real-world examples.

Microservices architecture has become the go-to approach for building scalable, maintainable applications. This guide explores essential patterns and practices for success.

Core Microservices Patterns

API Gateway Pattern

The API Gateway acts as a single entry point for all client requests:

# docker-compose.yml
version: "3.8"
services:
  api-gateway:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - user-service
      - order-service
      - product-service

  user-service:
    build: ./user-service
    ports:
      - "3001:3000"

  order-service:
    build: ./order-service
    ports:
      - "3002:3000"

  product-service:
    build: ./product-service
    ports:
      - "3003:3000"

NGINX configuration:

upstream user_service {
    server user-service:3000;
}

upstream order_service {
    server order-service:3000;
}

server {
    listen 80;

    location /api/users {
        proxy_pass http://user_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /api/orders {
        proxy_pass http://order_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Service Discovery

Implement service discovery for dynamic service registration:

// service-registry.js
class ServiceRegistry {
  constructor() {
    this.services = new Map();
    this.healthChecks = new Map();
  }

  register(serviceName, serviceUrl, healthCheckUrl) {
    this.services.set(serviceName, {
      url: serviceUrl,
      lastSeen: Date.now(),
      healthy: true,
    });

    this.healthChecks.set(serviceName, healthCheckUrl);
    this.startHealthCheck(serviceName);
  }

  discover(serviceName) {
    const service = this.services.get(serviceName);
    if (service && service.healthy) {
      return service.url;
    }
    throw new Error(`Service ${serviceName} not available`);
  }

  async startHealthCheck(serviceName) {
    const checkInterval = setInterval(async () => {
      try {
        const healthUrl = this.healthChecks.get(serviceName);
        const response = await fetch(healthUrl);

        if (response.ok) {
          this.services.get(serviceName).healthy = true;
          this.services.get(serviceName).lastSeen = Date.now();
        } else {
          this.services.get(serviceName).healthy = false;
        }
      } catch (error) {
        this.services.get(serviceName).healthy = false;
      }
    }, 30000); // Check every 30 seconds
  }
}

module.exports = ServiceRegistry;

Circuit Breaker Pattern

Prevent cascading failures with circuit breakers:

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.timeout = options.timeout || 60000;
    this.resetTimeout = options.resetTimeout || 60000;

    this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.lastFailureTime = null;
  }

  async call(serviceFunction) {
    if (this.state === "OPEN") {
      if (Date.now() - this.lastFailureTime > this.resetTimeout) {
        this.state = "HALF_OPEN";
      } else {
        throw new Error("Circuit breaker is OPEN");
      }
    }

    try {
      const result = await Promise.race([
        serviceFunction(),
        this.timeoutPromise(),
      ]);

      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = "CLOSED";
  }

  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (this.failureCount >= this.failureThreshold) {
      this.state = "OPEN";
    }
  }

  timeoutPromise() {
    return new Promise((_, reject) => {
      setTimeout(() => reject(new Error("Service timeout")), this.timeout);
    });
  }
}

// Usage
const breaker = new CircuitBreaker({
  failureThreshold: 3,
  timeout: 5000,
  resetTimeout: 30000,
});

async function callExternalService() {
  return breaker.call(async () => {
    const response = await fetch("http://external-service/api/data");
    if (!response.ok) throw new Error("Service failed");
    return response.json();
  });
}

Event-Driven Architecture

Event Sourcing

Store events instead of current state:

class EventStore {
  constructor() {
    this.events = [];
  }

  append(streamId, events) {
    const streamEvents = events.map((event) => ({
      ...event,
      streamId,
      timestamp: new Date().toISOString(),
      version: this.getNextVersion(streamId),
    }));

    this.events.push(...streamEvents);
    return streamEvents;
  }

  getEvents(streamId, fromVersion = 0) {
    return this.events.filter(
      (event) => event.streamId === streamId && event.version > fromVersion
    );
  }

  getNextVersion(streamId) {
    const streamEvents = this.events.filter((e) => e.streamId === streamId);
    return streamEvents.length;
  }
}

class OrderAggregate {
  constructor(orderId) {
    this.orderId = orderId;
    this.items = [];
    this.status = "created";
    this.version = 0;
  }

  static fromEvents(events) {
    const order = new OrderAggregate(events[0].streamId);
    events.forEach((event) => order.apply(event));
    return order;
  }

  addItem(productId, quantity, price) {
    const event = {
      type: "ItemAdded",
      data: { productId, quantity, price },
    };
    this.apply(event);
    return [event];
  }

  apply(event) {
    switch (event.type) {
      case "ItemAdded":
        this.items.push(event.data);
        break;
      case "OrderConfirmed":
        this.status = "confirmed";
        break;
      case "OrderShipped":
        this.status = "shipped";
        break;
    }
    this.version++;
  }
}

Message Queues with RabbitMQ

const amqp = require("amqplib");

class MessageBroker {
  constructor() {
    this.connection = null;
    this.channel = null;
  }

  async connect() {
    this.connection = await amqp.connect("amqp://localhost");
    this.channel = await this.connection.createChannel();
  }

  async publishEvent(exchange, routingKey, event) {
    await this.channel.assertExchange(exchange, "topic", { durable: true });

    const message = JSON.stringify(event);
    this.channel.publish(exchange, routingKey, Buffer.from(message), {
      persistent: true,
      timestamp: Date.now(),
      messageId: generateId(),
    });
  }

  async subscribeToEvents(exchange, routingKey, handler) {
    await this.channel.assertExchange(exchange, "topic", { durable: true });

    const queue = await this.channel.assertQueue("", { exclusive: true });
    await this.channel.bindQueue(queue.queue, exchange, routingKey);

    this.channel.consume(queue.queue, async (msg) => {
      if (msg) {
        try {
          const event = JSON.parse(msg.content.toString());
          await handler(event);
          this.channel.ack(msg);
        } catch (error) {
          console.error("Event processing failed:", error);
          this.channel.nack(msg, false, false); // Dead letter queue
        }
      }
    });
  }
}

// Usage in services
const broker = new MessageBroker();
await broker.connect();

// Order service publishes events
await broker.publishEvent("orders", "order.created", {
  orderId: "123",
  customerId: "456",
  items: [{ productId: "789", quantity: 2 }],
});

// Inventory service subscribes to events
await broker.subscribeToEvents("orders", "order.*", async (event) => {
  if (event.type === "order.created") {
    await updateInventory(event.items);
  }
});

Data Management Patterns

Database per Service

Each service owns its data:

// User Service - PostgreSQL
class UserService {
  constructor(pgPool) {
    this.db = pgPool;
  }

  async createUser(userData) {
    const query = `
            INSERT INTO users (id, email, name, created_at)
            VALUES ($1, $2, $3, $4)
            RETURNING *
        `;
    const result = await this.db.query(query, [
      userData.id,
      userData.email,
      userData.name,
      new Date(),
    ]);
    return result.rows[0];
  }
}

// Order Service - MongoDB
class OrderService {
  constructor(mongoClient) {
    this.db = mongoClient.db("orders");
  }

  async createOrder(orderData) {
    const order = {
      ...orderData,
      createdAt: new Date(),
      status: "pending",
    };

    const result = await this.db.collection("orders").insertOne(order);
    return { ...order, _id: result.insertedId };
  }
}

// Product Service - Redis
class ProductService {
  constructor(redisClient) {
    this.redis = redisClient;
  }

  async getProduct(productId) {
    const cached = await this.redis.get(`product:${productId}`);
    if (cached) return JSON.parse(cached);

    // Fetch from primary database
    const product = await this.fetchFromDatabase(productId);
    await this.redis.setex(
      `product:${productId}`,
      3600,
      JSON.stringify(product)
    );

    return product;
  }
}

Saga Pattern

Manage distributed transactions:

class OrderSaga {
  constructor(services) {
    this.userService = services.userService;
    this.inventoryService = services.inventoryService;
    this.paymentService = services.paymentService;
    this.orderService = services.orderService;
  }

  async processOrder(orderData) {
    const sagaId = generateId();
    const steps = [];

    try {
      // Step 1: Validate user
      const user = await this.userService.validateUser(orderData.userId);
      steps.push({
        action: "validateUser",
        data: { userId: orderData.userId },
      });

      // Step 2: Reserve inventory
      const reservation = await this.inventoryService.reserveItems(
        orderData.items
      );
      steps.push({ action: "reserveInventory", data: reservation });

      // Step 3: Process payment
      const payment = await this.paymentService.processPayment({
        userId: orderData.userId,
        amount: orderData.total,
      });
      steps.push({ action: "processPayment", data: payment });

      // Step 4: Create order
      const order = await this.orderService.createOrder({
        ...orderData,
        paymentId: payment.id,
        reservationId: reservation.id,
      });

      return order;
    } catch (error) {
      // Compensate in reverse order
      await this.compensate(steps.reverse());
      throw error;
    }
  }

  async compensate(steps) {
    for (const step of steps) {
      try {
        switch (step.action) {
          case "validateUser":
            // No compensation needed
            break;
          case "reserveInventory":
            await this.inventoryService.releaseReservation(step.data.id);
            break;
          case "processPayment":
            await this.paymentService.refundPayment(step.data.id);
            break;
        }
      } catch (compensationError) {
        console.error("Compensation failed:", compensationError);
      }
    }
  }
}

Monitoring and Observability

Distributed Tracing

const opentelemetry = require("@opentelemetry/api");
const { NodeSDK } = require("@opentelemetry/auto-instrumentations-node");

// Initialize tracing
const sdk = new NodeSDK({
  serviceName: "order-service",
  instrumentations: [],
});

sdk.start();

// Custom spans
async function processOrder(orderData) {
  const tracer = opentelemetry.trace.getTracer("order-service");

  return tracer.startActiveSpan("process-order", async (span) => {
    try {
      span.setAttributes({
        "order.id": orderData.id,
        "order.customerId": orderData.customerId,
        "order.total": orderData.total,
      });

      const result = await createOrder(orderData);
      span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
      return result;
    } catch (error) {
      span.recordException(error);
      span.setStatus({
        code: opentelemetry.SpanStatusCode.ERROR,
        message: error.message,
      });
      throw error;
    } finally {
      span.end();
    }
  });
}

Health Checks

class HealthChecker {
  constructor() {
    this.checks = new Map();
  }

  addCheck(name, checkFunction) {
    this.checks.set(name, checkFunction);
  }

  async getStatus() {
    const results = {};
    const promises = Array.from(this.checks.entries()).map(
      async ([name, check]) => {
        try {
          const startTime = Date.now();
          await check();
          results[name] = {
            status: "healthy",
            responseTime: Date.now() - startTime,
          };
        } catch (error) {
          results[name] = {
            status: "unhealthy",
            error: error.message,
          };
        }
      }
    );

    await Promise.all(promises);

    const overall = Object.values(results).every((r) => r.status === "healthy")
      ? "healthy"
      : "unhealthy";

    return {
      status: overall,
      timestamp: new Date().toISOString(),
      checks: results,
    };
  }
}

// Usage
const healthChecker = new HealthChecker();

healthChecker.addCheck("database", async () => {
  await db.query("SELECT 1");
});

healthChecker.addCheck("external-api", async () => {
  const response = await fetch("http://external-service/health");
  if (!response.ok) throw new Error("External service unavailable");
});

app.get("/health", async (req, res) => {
  const status = await healthChecker.getStatus();
  res.status(status.status === "healthy" ? 200 : 503).json(status);
});

Best Practices

Service Communication

  1. Prefer Async Communication: Use events over direct HTTP calls
  2. Implement Timeouts: Always set reasonable timeouts
  3. Use Bulkheads: Isolate critical resources
  4. Plan for Failures: Every external call can fail

Data Consistency

  1. Embrace Eventual Consistency: Design for it from the start
  2. Use Idempotent Operations: Make retries safe
  3. Implement Compensation: Plan rollback strategies
  4. Monitor Data Drift: Track consistency across services

Security

  1. Service-to-Service Authentication: Use mTLS or JWT
  2. API Gateway Security: Centralize authentication/authorization
  3. Network Segmentation: Isolate services with firewalls
  4. Secret Management: Use dedicated secret stores

Conclusion

Microservices architecture offers powerful benefits but requires careful design and implementation. By following these patterns and practices, you can build resilient, scalable systems that deliver business value while maintaining operational simplicity.