正在加载,请稍候…

Microservices Architecture Patterns: When to Use Them and How to Implement

Practical guide to microservices: service decomposition, API gateway, service discovery, circuit breaker, saga pattern, and event-driven architecture. With real implementation examples.

Microservices Are Not a Goal, They're a Trade-Off

The microservices hype peaked years ago. The honest reality: microservices solve specific organizational and scaling problems while introducing significant complexity. Running 50 services instead of one monolith means 50 deployment pipelines, 50 monitoring dashboards, and a distributed systems problem on every request.

Before adopting microservices, ask: is your team large enough that coordination on a monolith is genuinely painful? Are specific parts of your system scaling at dramatically different rates? If the answer is no, a well-structured monolith will serve you better.

This guide assumes you've decided microservices are appropriate and explains how to implement them well.

Service Decomposition: Where to Draw Boundaries

The single hardest problem in microservices. Get this wrong and you'll have a "distributed monolith" — all the complexity of microservices with none of the benefits.

Domain-Driven Design (DDD) Bounded Contexts provide the most reliable decomposition strategy:

E-commerce System Bounded Contexts:

Order Context          User Context          Inventory Context
├── Order               ├── User              ├── Product
├── OrderLine           ├── Address           ├── Stock
├── OrderStatus         ├── Payment Method    ├── Warehouse
└── Shipping            └── Profile           └── Reservation

Payment Context        Notification Context
├── Transaction         ├── Email
├── Refund              ├── SMS
└── Invoice             └── Push Notification

Each bounded context becomes a service. The key: services should be able to change independently. If changing the Order service always requires changing the User service, the boundary is wrong.

API Gateway Pattern

External clients shouldn't call individual services directly:

// Simple API Gateway with Express
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

// Authentication middleware (runs before all proxies)
app.use(async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });
  
  try {
    req.user = await verifyJWT(token);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
});

// Route to appropriate microservice
app.use('/api/users', createProxyMiddleware({
  target: process.env.USER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

app.use('/api/orders', createProxyMiddleware({
  target: process.env.ORDER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
}));

// Aggregation: combine multiple services
app.get('/api/dashboard', async (req, res) => {
  const [user, orders, notifications] = await Promise.all([
    fetch(`${USER_SERVICE}/users/${req.user.id}`).then(r => r.json()),
    fetch(`${ORDER_SERVICE}/users/${req.user.id}/recent`).then(r => r.json()),
    fetch(`${NOTIF_SERVICE}/users/${req.user.id}/unread`).then(r => r.json()),
  ]);
  
  res.json({ user, orders, notifications });
});

Service-to-Service Communication

Synchronous: REST / gRPC

// Typed service client (gRPC style)
import { createChannel, createClient } from 'nice-grpc';
import { OrderServiceDefinition } from './proto/order';

const channel = createChannel('order-service:50051');
const orderClient = createClient(OrderServiceDefinition, channel);

// Type-safe, efficient binary protocol
const order = await orderClient.getOrder({ orderId: '123' });
const { stream } = orderClient.watchOrderStatus({ orderId: '123' });

for await (const status of stream) {
  console.log('Order status:', status);
}

Asynchronous: Message Queues

// RabbitMQ with amqplib
import amqp from 'amqplib';

const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();

// Publisher: Order Service
async function publishOrderCreated(order) {
  await channel.assertExchange('orders', 'topic', { durable: true });
  
  channel.publish(
    'orders',
    'order.created',
    Buffer.from(JSON.stringify({
      orderId: order.id,
      userId: order.userId,
      total: order.total,
      timestamp: new Date().toISOString(),
    })),
    { persistent: true }
  );
}

// Subscriber: Inventory Service
await channel.assertExchange('orders', 'topic', { durable: true });
const { queue } = await channel.assertQueue('inventory.order-created');
await channel.bindQueue(queue, 'orders', 'order.created');

channel.consume(queue, async (msg) => {
  const event = JSON.parse(msg.content.toString());
  
  try {
    await reserveInventory(event.orderId);
    channel.ack(msg); // Acknowledge success
  } catch (error) {
    // Negative ack — requeue for retry
    channel.nack(msg, false, true);
  }
});

Circuit Breaker Pattern

Prevent cascade failures when a downstream service is struggling:

import CircuitBreaker from 'opossum';

// Wrap any async function in a circuit breaker
const paymentCircuitBreaker = new CircuitBreaker(
  async (paymentData) => {
    return fetch(`${PAYMENT_SERVICE}/charge`, {
      method: 'POST',
      body: JSON.stringify(paymentData),
      signal: AbortSignal.timeout(3000), // 3 second timeout
    }).then(r => r.json());
  },
  {
    timeout: 3000,          // How long before considering a request failed
    errorThresholdPercentage: 50,  // Open circuit if 50%+ fail
    resetTimeout: 30000,    // Try again after 30 seconds
    volumeThreshold: 5,     // Minimum requests before opening
  }
);

paymentCircuitBreaker.on('open', () => {
  console.error('Payment service circuit OPEN — using fallback');
});
paymentCircuitBreaker.on('halfOpen', () => {
  console.log('Payment service circuit testing...');
});
paymentCircuitBreaker.on('close', () => {
  console.log('Payment service circuit CLOSED — normal operation');
});

// Usage
async function processPayment(orderId, amount) {
  try {
    return await paymentCircuitBreaker.fire({ orderId, amount });
  } catch (error) {
    if (paymentCircuitBreaker.opened) {
      // Fallback: queue for later processing
      await queuePaymentForRetry({ orderId, amount });
      return { status: 'queued', message: 'Payment queued for processing' };
    }
    throw error;
  }
}

Saga Pattern: Distributed Transactions

When an operation spans multiple services, you need sagas instead of ACID transactions:

// Choreography-based Saga: services react to events
// (No central coordinator — services emit and react)

// Order Service creates order → publishes OrderCreated
async function createOrder(orderData) {
  const order = await db.orders.create({ ...orderData, status: 'pending' });
  await eventBus.publish('OrderCreated', { orderId: order.id, ...orderData });
  return order;
}

// Payment Service hears OrderCreated → charges payment
eventBus.subscribe('OrderCreated', async (event) => {
  try {
    await chargePayment(event.userId, event.total);
    await eventBus.publish('PaymentProcessed', { orderId: event.orderId });
  } catch (error) {
    // Compensating transaction
    await eventBus.publish('PaymentFailed', { orderId: event.orderId });
  }
});

// Order Service hears PaymentFailed → cancels order
eventBus.subscribe('PaymentFailed', async (event) => {
  await db.orders.update(event.orderId, { status: 'cancelled' });
  await eventBus.publish('OrderCancelled', { orderId: event.orderId });
});

// Inventory Service hears PaymentProcessed → reserves items
// Inventory Service hears OrderCancelled → releases items
// Orchestration-based Saga: central coordinator
class CreateOrderSaga {
  async execute(orderData) {
    const steps = [];
    
    try {
      // Step 1: Create order
      const order = await this.orderService.create(orderData);
      steps.push(() => this.orderService.cancel(order.id));
      
      // Step 2: Reserve inventory
      await this.inventoryService.reserve(order.items);
      steps.push(() => this.inventoryService.release(order.items));
      
      // Step 3: Process payment
      await this.paymentService.charge(order.userId, order.total);
      steps.push(() => this.paymentService.refund(order.userId, order.total));
      
      // Step 4: Confirm
      await this.orderService.confirm(order.id);
      
      return order;
    } catch (error) {
      // Rollback: execute compensating transactions in reverse
      for (const compensate of steps.reverse()) {
        await compensate().catch(console.error);
      }
      throw error;
    }
  }
}

Service Discovery

// Using Consul for service discovery
import Consul from 'consul';

const consul = new Consul({ host: 'consul', port: 8500 });

// Register on startup
await consul.agent.service.register({
  id: `order-service-${process.env.POD_IP}`,
  name: 'order-service',
  address: process.env.POD_IP,
  port: 3000,
  check: {
    http: `http://${process.env.POD_IP}:3000/health`,
    interval: '10s',
    deregisterCriticalServiceAfter: '30s',
  },
  tags: ['api', 'v2'],
});

// Discover and call another service
async function callUserService(userId) {
  // Get healthy instances of user-service
  const services = await consul.health.service({
    service: 'user-service',
    passing: true, // Only healthy instances
  });
  
  if (services.length === 0) throw new Error('No user-service instances available');
  
  // Simple round-robin (in practice, use a load-balancer or service mesh)
  const instance = services[Math.floor(Math.random() * services.length)];
  const url = `http://${instance.Service.Address}:${instance.Service.Port}`;
  
  return fetch(`${url}/users/${userId}`).then(r => r.json());
}

Key Trade-offs Summary

Concern Monolith Microservices
Complexity Low High (distributed systems)
Deployment Simple Complex (many pipelines)
Scaling Whole app Per-service
Team size 1-15 50+ (Conway's Law)
Latency In-process Network hops
Transactions ACID Saga pattern needed
Debugging Simple Distributed tracing required
Data isolation Shared DB Per-service DB

→ Convert service configs between YAML and JSON with the JSON to YAML Converter.