Microservices Architecture Guide
Introduction
Microservices architecture is an approach to developing software applications as a suite of independently deployable, small, modular services. Each service runs in its own process and communicates through well-defined APIs.
Monolithic vs Microservices
| Aspect | Monolithic | Microservices |
|---|---|---|
| Deployment | Single deployable unit | Multiple independent services |
| Scaling | Scale entire application | Scale individual services |
| Technology | Single technology stack | Polyglot programming |
| Team Structure | Large, centralized teams | Small, autonomous teams |
| Failure Impact | Can affect entire system | Isolated to service |
| Complexity | Simple deployment, complex codebase | Complex deployment, simple services |
Core Principles
Successful microservices architectures are built on fundamental principles:
- Single Responsibility: Each service should have one reason to change
- Autonomous Teams: Teams own their services end-to-end
- Decentralized Governance: Services choose their own tech stack
- Failure Isolation: Service failures don't cascade
- Data Decentralization: Each service manages its own data
- Smart Endpoints: Business logic in services, not middleware
- Design for Failure: Assume everything will fail
Architecture Overview
A typical microservices architecture consists of multiple components working together:
┌─────────────────────────────────────────────────────────┐
│ Client Applications │
│ (Web, Mobile, Desktop, Third-party) │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ API Gateway │
│ (Authentication, Routing, Rate Limiting) │
└────┬────────┬────────┬────────┬────────┬──────────────┘
│ │ │ │ │
┌────▼───┐ ┌─▼───┐ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐
│Service │ │ Svc │ │ Svc │ │ Svc │ │ Svc │
│ A │ │ B │ │ C │ │ D │ │ E │
└────┬───┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
│ │ │ │ │
┌────▼────────▼───────▼───────▼───────▼──┐
│ Service Mesh (Optional) │
│ (Service Discovery, Load Balancing) │
└─────────────────────────────────────────┘
│ │ │ │ │
┌────▼───┐ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐ ┌──▼──┐
│ DB A │ │ DB B│ │Cache│ │Queue│ │Store│
└────────┘ └─────┘ └─────┘ └─────┘ └─────┘
Key Components
- API Gateway: Single entry point for all client requests
- Service Registry: Dynamic service discovery and health checking
- Configuration Service: Centralized configuration management
- Circuit Breaker: Prevents cascading failures
- Service Mesh: Infrastructure layer for service-to-service communication
Service Design
Designing microservices requires careful consideration of boundaries and responsibilities:
Service Boundaries
# E-commerce platform service boundaries
services:
- name: user-service
responsibilities:
- User registration and authentication
- Profile management
- Preferences and settings
- name: product-service
responsibilities:
- Product catalog management
- Inventory tracking
- Product search and filtering
- name: order-service
responsibilities:
- Order placement and management
- Order status tracking
- Order history
- name: payment-service
responsibilities:
- Payment processing
- Payment method management
- Transaction history
- name: notification-service
responsibilities:
- Email notifications
- SMS notifications
- Push notifications
Service Interface Design
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
paths:
/users:
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/users/{userId}:
get:
summary: Get user by ID
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
Service Communication
Microservices communicate through various patterns and protocols:
Synchronous Communication
const CircuitBreaker = require('opossum');
const axios = require('axios');
class UserServiceClient {
constructor() {
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
this.breaker = new CircuitBreaker(this.callUserService, options);
this.breaker.on('open', () =>
console.log('Circuit breaker is open'));
this.breaker.on('halfOpen', () =>
console.log('Circuit breaker is half-open'));
this.breaker.on('close', () =>
console.log('Circuit breaker is closed'));
}
async getUser(userId) {
return this.breaker.fire(userId);
}
async callUserService(userId) {
const response = await axios.get(
`http://user-service:8080/users/${userId}`,
{
timeout: 2000,
headers: {
'X-Request-ID': generateRequestId(),
'X-Caller-Service': 'order-service'
}
}
);
return response.data;
}
}
Asynchronous Communication
// Event Publisher
class OrderService {
async createOrder(orderData) {
// Create order in database
const order = await this.orderRepository.create(orderData);
// Publish order created event
await this.eventBus.publish('order.created', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
timestamp: new Date().toISOString()
});
return order;
}
}
// Event Consumer
class InventoryService {
constructor() {
this.eventBus.subscribe('order.created', this.handleOrderCreated);
}
async handleOrderCreated(event) {
console.log(`Processing order ${event.orderId}`);
// Update inventory
for (const item of event.items) {
await this.inventoryRepository.decrementStock(
item.productId,
item.quantity
);
}
// Publish inventory updated event
await this.eventBus.publish('inventory.updated', {
orderId: event.orderId,
items: event.items,
timestamp: new Date().toISOString()
});
}
}
Data Management
Each microservice should manage its own data to ensure loose coupling:
Database per Service Pattern
- Each service owns its database schema
- No direct database access between services
- Data is accessed only through service APIs
- Choose the right database for each service's needs
Data Consistency Patterns
Saga Pattern
Manage distributed transactions across multiple services using choreography or orchestration.
Learn more →Event Sourcing
Store all changes as a sequence of events, enabling audit trails and temporal queries.
Learn more →Deployment Patterns
Microservices can be deployed using various strategies:
Container Orchestration with Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry/user-service:v1.0.0
ports:
- containerPort: 8080
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: user-db-secret
key: host
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
Service Mesh Integration
- Automatic service discovery and load balancing
- Encrypted service-to-service communication
- Advanced traffic management and routing
- Distributed tracing and monitoring
Monitoring & Observability
Comprehensive monitoring is crucial for microservices architectures:
The Three Pillars of Observability
Metrics
Quantitative data about system performance: latency, throughput, error rates, and resource utilization.
Logs
Detailed records of events and errors, structured for easy searching and correlation across services.
Traces
End-to-end request flow across multiple services, showing latency and dependencies.
Distributed Tracing Implementation
const { NodeTracerProvider } = require('@opentelemetry/node');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
// Initialize tracing
const provider = new NodeTracerProvider();
provider.register();
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
// Middleware to add trace context
app.use((req, res, next) => {
const span = tracer.startSpan('http_request', {
attributes: {
'http.method': req.method,
'http.url': req.url,
'http.target': req.path,
'service.name': 'user-service'
}
});
// Add trace ID to response headers
res.setHeader('X-Trace-ID', span.spanContext().traceId);
res.on('finish', () => {
span.setAttributes({
'http.status_code': res.statusCode
});
span.end();
});
next();
});
Common Patterns
Essential patterns for building resilient microservices:
Saga Pattern
// Order Service - Initiates the saga
class OrderSaga {
async createOrder(orderData) {
const order = await this.orderService.create({
...orderData,
status: 'PENDING'
});
// Start saga by publishing event
await this.eventBus.publish('OrderCreated', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total
});
return order;
}
// Compensating transaction
async cancelOrder(orderId, reason) {
await this.orderService.updateStatus(orderId, 'CANCELLED');
await this.eventBus.publish('OrderCancelled', {
orderId,
reason,
timestamp: new Date()
});
}
}
// Payment Service - Participates in saga
class PaymentSaga {
constructor() {
this.eventBus.subscribe('OrderCreated', this.processPayment);
this.eventBus.subscribe('InventoryReserved', this.confirmPayment);
this.eventBus.subscribe('InventoryFailed', this.cancelPayment);
}
async processPayment(event) {
try {
const payment = await this.paymentService.charge({
userId: event.userId,
amount: event.total,
orderId: event.orderId
});
await this.eventBus.publish('PaymentProcessed', {
orderId: event.orderId,
paymentId: payment.id,
status: 'SUCCESS'
});
} catch (error) {
await this.eventBus.publish('PaymentFailed', {
orderId: event.orderId,
reason: error.message
});
}
}
}
API Gateway Pattern
- Single entry point for all client requests
- Request routing and load balancing
- Authentication and authorization
- Rate limiting and throttling
- Request/response transformation
- API versioning and deprecation
Bulkhead Pattern
Isolate resources to prevent cascading failures:
@Component
public class PaymentServiceClient {
private final ExecutorService paymentExecutor =
Executors.newFixedThreadPool(10); // Bulkhead of 10 threads
private final ExecutorService inventoryExecutor =
Executors.newFixedThreadPool(5); // Bulkhead of 5 threads
public CompletableFuture processPayment(PaymentRequest request) {
return CompletableFuture.supplyAsync(() -> {
// Payment processing logic
return paymentService.process(request);
}, paymentExecutor)
.orTimeout(5, TimeUnit.SECONDS);
}
public CompletableFuture checkInventory(String productId) {
return CompletableFuture.supplyAsync(() -> {
// Inventory check logic
return inventoryService.check(productId);
}, inventoryExecutor)
.orTimeout(3, TimeUnit.SECONDS);
}
}
Best Practices
Follow these practices for successful microservices implementations:
Design Best Practices
- Design services around business capabilities
- Keep services small and focused
- Ensure services are independently deployable
- Use domain-driven design principles
- Implement proper API versioning
- Document all service interfaces
Operational Best Practices
- Implement comprehensive monitoring and logging
- Use distributed tracing for debugging
- Automate testing at all levels
- Implement circuit breakers and retries
- Use health checks and graceful shutdowns
- Plan for service discovery and load balancing