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
- Prefer Async Communication: Use events over direct HTTP calls
- Implement Timeouts: Always set reasonable timeouts
- Use Bulkheads: Isolate critical resources
- Plan for Failures: Every external call can fail
Data Consistency
- Embrace Eventual Consistency: Design for it from the start
- Use Idempotent Operations: Make retries safe
- Implement Compensation: Plan rollback strategies
- Monitor Data Drift: Track consistency across services
Security
- Service-to-Service Authentication: Use mTLS or JWT
- API Gateway Security: Centralize authentication/authorization
- Network Segmentation: Isolate services with firewalls
- 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.