Microservices Architecture
Distributed Systems Done Right
Explanation
What are Microservices?
Microservices architecture structures an application as a collection of loosely coupled services. Each service is independently deployable and organized around business capabilities.
Key Characteristics
| Characteristic | Description | |----------------|-------------| | Single Responsibility | Each service does one thing well | | Independent Deployment | Deploy without affecting others | | Decentralized Data | Each service owns its data | | Technology Agnostic | Use the best tool for each job | | Resilient | Failure isolation |
Monolith vs Microservices
Monolith: Microservices:
┌─────────────────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ UI + Business │ │User │ │Order│ │Pay- │
│ Logic + Data │ vs │Svc │ │Svc │ │ment │
└─────────────────┘ └─────┘ └─────┘ └─────┘
└──────┬───────┘
Message Bus
Demonstration
Example 1: Service Communication (REST)
// User Service
const express = require('express');
const app = express();
// User endpoints
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.post('/users', async (req, res) => {
const user = await db.users.create(req.body);
// Publish event
await messageBus.publish('user.created', user);
res.status(201).json(user);
});
// Order Service - calling User Service
const axios = require('axios');
const userServiceUrl = process.env.USER_SERVICE_URL || 'http://user-service:3000';
async function getUserById(userId) {
try {
const response = await axios.get(`${userServiceUrl}/users/${userId}`, {
timeout: 5000,
headers: {
'X-Request-Id': req.requestId
}
});
return response.data;
} catch (error) {
if (error.response?.status === 404) {
throw new UserNotFoundError(userId);
}
throw new ServiceError('user-service', error);
}
}
app.post('/orders', async (req, res) => {
// Get user from User Service
const user = await getUserById(req.body.userId);
// Create order
const order = await db.orders.create({
userId: user.id,
items: req.body.items,
total: calculateTotal(req.body.items)
});
// Publish event
await messageBus.publish('order.created', order);
res.status(201).json(order);
});
Example 2: Event-Driven Communication
// Message broker setup (RabbitMQ)
const amqp = require('amqplib');
class MessageBus {
constructor() {
this.connection = null;
this.channel = null;
}
async connect() {
this.connection = await amqp.connect(process.env.RABBITMQ_URL);
this.channel = await this.connection.createChannel();
}
async publish(event, data) {
const exchange = 'events';
await this.channel.assertExchange(exchange, 'topic', { durable: true });
this.channel.publish(
exchange,
event,
Buffer.from(JSON.stringify({
event,
data,
timestamp: new Date().toISOString(),
correlationId: asyncLocalStorage.getStore()?.requestId
}))
);
}
async subscribe(pattern, handler) {
const exchange = 'events';
await this.channel.assertExchange(exchange, 'topic', { durable: true });
const { queue } = await this.channel.assertQueue('', { exclusive: true });
await this.channel.bindQueue(queue, exchange, pattern);
this.channel.consume(queue, async (msg) => {
const content = JSON.parse(msg.content.toString());
try {
await handler(content);
this.channel.ack(msg);
} catch (error) {
console.error('Handler error:', error);
// Requeue or send to dead letter queue
this.channel.nack(msg, false, false);
}
});
}
}
// User Service - publishes events
app.post('/users', async (req, res) => {
const user = await db.users.create(req.body);
await messageBus.publish('user.created', {
id: user.id,
email: user.email,
name: user.name
});
res.status(201).json(user);
});
// Email Service - subscribes to events
await messageBus.subscribe('user.created', async (message) => {
const { data: user } = message;
await sendWelcomeEmail({
to: user.email,
name: user.name
});
console.log(`Welcome email sent to ${user.email}`);
});
// Order Service - subscribes to payment events
await messageBus.subscribe('payment.*', async (message) => {
const { event, data } = message;
switch (event) {
case 'payment.completed':
await db.orders.update(data.orderId, { status: 'paid' });
await messageBus.publish('order.paid', { orderId: data.orderId });
break;
case 'payment.failed':
await db.orders.update(data.orderId, { status: 'payment_failed' });
break;
}
});
Example 3: API Gateway
// API Gateway with Express
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const app = express();
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(limiter);
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
// Service routes
const services = {
users: process.env.USER_SERVICE_URL,
orders: process.env.ORDER_SERVICE_URL,
products: process.env.PRODUCT_SERVICE_URL,
payments: process.env.PAYMENT_SERVICE_URL
};
// Proxy configuration
Object.entries(services).forEach(([name, target]) => {
app.use(
`/api/${name}`,
authenticate,
createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: { [`^/api/${name}`]: '' },
onProxyReq: (proxyReq, req) => {
// Forward user info
proxyReq.setHeader('X-User-Id', req.user.id);
proxyReq.setHeader('X-Request-Id', req.requestId);
}
})
);
});
// Aggregation endpoint
app.get('/api/dashboard', authenticate, async (req, res) => {
const [user, orders, notifications] = await Promise.all([
fetch(`${services.users}/users/${req.user.id}`).then(r => r.json()),
fetch(`${services.orders}/orders?userId=${req.user.id}&limit=5`).then(r => r.json()),
fetch(`${services.notifications}/notifications?userId=${req.user.id}&unread=true`).then(r => r.json())
]);
res.json({
user,
recentOrders: orders,
unreadNotifications: notifications.length
});
});
Example 4: Service Discovery
// Consul-based service discovery
const Consul = require('consul');
const consul = new Consul({
host: process.env.CONSUL_HOST || 'localhost',
port: process.env.CONSUL_PORT || 8500
});
class ServiceRegistry {
constructor(serviceName, servicePort) {
this.serviceName = serviceName;
this.servicePort = servicePort;
this.serviceId = `${serviceName}-${process.env.HOSTNAME}`;
}
async register() {
await consul.agent.service.register({
id: this.serviceId,
name: this.serviceName,
address: process.env.HOSTNAME,
port: this.servicePort,
check: {
http: `http://${process.env.HOSTNAME}:${this.servicePort}/health`,
interval: '10s',
timeout: '5s'
}
});
// Deregister on shutdown
process.on('SIGTERM', () => this.deregister());
process.on('SIGINT', () => this.deregister());
}
async deregister() {
await consul.agent.service.deregister(this.serviceId);
process.exit(0);
}
async getService(serviceName) {
const services = await consul.health.service({
service: serviceName,
passing: true
});
if (services.length === 0) {
throw new Error(`No healthy instances of ${serviceName}`);
}
// Simple round-robin
const index = Math.floor(Math.random() * services.length);
const service = services[index].Service;
return `http://${service.Address}:${service.Port}`;
}
}
// Usage
const registry = new ServiceRegistry('order-service', 3000);
await registry.register();
// Call another service
async function callUserService(userId) {
const userServiceUrl = await registry.getService('user-service');
return fetch(`${userServiceUrl}/users/${userId}`);
}
Example 5: Circuit Breaker
// Circuit breaker implementation
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.state = 'CLOSED';
this.failures = 0;
this.lastFailureTime = null;
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new CircuitBreakerError('Circuit is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
getState() {
return {
state: this.state,
failures: this.failures,
nextAttempt: this.state === 'OPEN' ? this.nextAttempt : null
};
}
}
// Usage
const userServiceBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000
});
async function getUserWithCircuitBreaker(userId) {
return userServiceBreaker.execute(async () => {
const response = await fetch(`${userServiceUrl}/users/${userId}`, {
timeout: 5000
});
if (!response.ok) throw new Error('Service error');
return response.json();
});
}
// With fallback
async function getUserSafe(userId) {
try {
return await getUserWithCircuitBreaker(userId);
} catch (error) {
if (error instanceof CircuitBreakerError) {
// Return cached or default data
return getCachedUser(userId) || { id: userId, name: 'Unknown' };
}
throw error;
}
}
Example 6: Saga Pattern
// Saga for order processing
class OrderSaga {
constructor(services, messageBus) {
this.services = services;
this.messageBus = messageBus;
}
async execute(orderData) {
const saga = {
orderId: null,
steps: [],
status: 'pending'
};
try {
// Step 1: Create order
saga.orderId = await this.createOrder(orderData);
saga.steps.push({ step: 'createOrder', status: 'completed' });
// Step 2: Reserve inventory
await this.reserveInventory(saga.orderId, orderData.items);
saga.steps.push({ step: 'reserveInventory', status: 'completed' });
// Step 3: Process payment
await this.processPayment(saga.orderId, orderData.paymentMethod, orderData.total);
saga.steps.push({ step: 'processPayment', status: 'completed' });
// Step 4: Confirm order
await this.confirmOrder(saga.orderId);
saga.steps.push({ step: 'confirmOrder', status: 'completed' });
saga.status = 'completed';
return saga;
} catch (error) {
saga.status = 'failed';
saga.error = error.message;
// Compensate - rollback completed steps in reverse
await this.compensate(saga);
throw error;
}
}
async createOrder(orderData) {
const order = await this.services.orders.create(orderData);
return order.id;
}
async reserveInventory(orderId, items) {
await this.services.inventory.reserve(orderId, items);
}
async processPayment(orderId, paymentMethod, amount) {
await this.services.payments.charge(orderId, paymentMethod, amount);
}
async confirmOrder(orderId) {
await this.services.orders.confirm(orderId);
}
async compensate(saga) {
const completedSteps = saga.steps
.filter(s => s.status === 'completed')
.reverse();
for (const step of completedSteps) {
try {
switch (step.step) {
case 'processPayment':
await this.services.payments.refund(saga.orderId);
break;
case 'reserveInventory':
await this.services.inventory.release(saga.orderId);
break;
case 'createOrder':
await this.services.orders.cancel(saga.orderId);
break;
}
step.compensated = true;
} catch (error) {
console.error(`Compensation failed for ${step.step}:`, error);
// Log for manual intervention
}
}
}
}
// Usage
const orderSaga = new OrderSaga(services, messageBus);
app.post('/orders', async (req, res) => {
try {
const result = await orderSaga.execute(req.body);
res.status(201).json({ orderId: result.orderId });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Key Takeaways:
- Design services around business capabilities
- Use async communication when possible
- Implement circuit breakers for resilience
- Use sagas for distributed transactions
- API gateways centralize cross-cutting concerns
Imitation
Challenge 1: Build an E-commerce System
Task: Design microservices for an e-commerce platform.
Solution
Services:
1. User Service - authentication, profiles
2. Product Service - catalog, search
3. Cart Service - shopping cart
4. Order Service - order management
5. Payment Service - payment processing
6. Inventory Service - stock management
7. Notification Service - emails, SMS
8. Shipping Service - delivery tracking
Communication:
- REST for synchronous (cart → product)
- Events for async (order.created → inventory, notification)
Events:
- user.registered → notification (welcome email)
- order.created → inventory (reserve), payment (charge)
- payment.completed → order (confirm), notification (receipt)
- payment.failed → inventory (release), notification (alert)
- order.shipped → notification (tracking)
Practice
Exercise 1: Event Sourcing
Difficulty: Intermediate
Implement event sourcing:
- Store events as source of truth
- Rebuild state from events
- Snapshots for performance
Exercise 2: Service Mesh
Difficulty: Advanced
Set up a service mesh:
- Sidecar proxies
- Mutual TLS
- Traffic management
Summary
What you learned:
- Microservices architecture
- Communication patterns
- Service discovery
- Circuit breakers
- Saga pattern
Next Steps:
- Read: API Design
- Practice: Split a monolith
- Explore: Kubernetes, Istio
