System Design: Building Scalable Microservices
Introduction
Microservices architecture has become the standard for building large-scale, resilient applications. However, designing microservices properly requires understanding various patterns, trade-offs, and implementation details.
Core Principles
1. Service Independence
Each microservice should:
- Have its own database
- Be independently deployable
- Use technology appropriate for its domain
- Have a single responsibility
2. Communication Patterns
Synchronous Communication (HTTP/REST):
typescript1// Service A calling Service B 2async function getUserOrders(userId: string) { 3 const response = await fetch(`http://orders-service/api/orders?userId=${userId}`); 4 return response.json(); 5}
Asynchronous Communication (Message Queues):
typescript1// Using RabbitMQ with AMQP 2import amqp from 'amqplib'; 3 4async function publishOrderCreated(order: Order) { 5 const connection = await amqp.connect('amqp://localhost'); 6 const channel = await connection.createChannel(); 7 8 await channel.assertExchange('order-events', 'topic', { durable: true }); 9 channel.publish('order-events', 'order.created', Buffer.from(JSON.stringify(order))); 10}
Database Design Patterns
Database per Service
Each service has its own database, preventing tight coupling:
yaml1services: 2 users-service: 3 image: users-service:latest 4 environment: 5 - DATABASE_URL=postgresql://localhost/users_db 6 7 orders-service: 8 image: orders-service:latest 9 environment: 10 - DATABASE_URL=postgresql://localhost/orders_db
Saga Pattern for Distributed Transactions
When you need ACID transactions across services:
typescript1class CreateOrderSaga { 2 async execute(orderData: OrderData) { 3 // 1. Start transaction 4 await this.reserveInventory(orderData); 5 6 // 2. Process payment 7 const paymentResult = await this.processPayment(orderData); 8 9 if (!paymentResult.success) { 10 // 3. Compensate: release inventory 11 await this.releaseInventory(orderData); 12 throw new Error('Payment failed'); 13 } 14 15 // 4. Create order 16 return this.createOrder(orderData); 17 } 18}
Service Discovery & Load Balancing
Using Consul for Service Discovery:
typescript1import Consul from 'consul'; 2 3const consul = new Consul({ host: 'consul-server' }); 4 5async function discoverService(serviceName: string) { 6 const services = await consul.agent.service.list(); 7 const service = Object.values(services).find(s => s.Service === serviceName); 8 9 if (!service) { 10 throw new Error(`Service ${serviceName} not found`); 11 } 12 13 return { 14 host: service.Address, 15 port: service.Port 16 }; 17}
Monitoring & Observability
Distributed Tracing with Jaeger
typescript1import { initTracer } from 'jaeger-client'; 2 3const config = { 4 serviceName: 'orders-service', 5 sampler: { 6 type: 'const', 7 param: 1, 8 }, 9 reporter: { 10 logSpans: true, 11 agentHost: 'jaeger-agent', 12 }, 13}; 14 15const tracer = initTracer(config); 16 17async function processOrder(order: Order) { 18 const span = tracer.startSpan('process-order'); 19 20 try { 21 // Business logic 22 span.setTag('order.id', order.id); 23 span.setTag('order.amount', order.amount); 24 25 // ... processing logic 26 27 span.finish(); 28 } catch (error) { 29 span.setTag('error', true); 30 span.log({ event: 'error', message: error.message }); 31 span.finish(); 32 throw error; 33 } 34}
Deployment Strategy
Blue-Green Deployment:
yaml1# docker-compose.yml 2version: '3.8' 3services: 4 app-blue: 5 image: myapp:blue 6 ports: 7 - "8080:8080" 8 9 app-green: 10 image: myapp:green 11 ports: 12 - "8081:8080" 13 14 nginx: 15 image: nginx 16 volumes: 17 - ./nginx.conf:/etc/nginx/nginx.conf