Microservices Architecture
As applications grow, monolithic architectures become difficult to maintain and scale. Microservices offer a way to build applications as a collection of small, independent services.Monolith vs Microservices
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment | All or nothing | Independent per service |
| Scaling | Scale entire app | Scale specific services |
| Technology | Single stack | Best tool for each job |
| Team Structure | Large, coordinated team | Small, autonomous teams |
| Complexity | In the code | In the infrastructure |
| Data | Single database | Database per service |
When to Use Microservices
✅ Use when:- Large team (50+ developers)
- Different scaling needs per feature
- Need technology diversity
- High availability requirements
- Complex domain with clear boundaries
- Small team or startup MVP
- Simple CRUD application
- No clear service boundaries
- Limited DevOps capabilities
Service Design Principles
Single Responsibility
Each service should do one thing well.Copy
✅ Good Service Boundaries:
- User Service (auth, profiles)
- Order Service (cart, checkout)
- Payment Service (transactions)
- Notification Service (email, SMS, push)
- Inventory Service (stock management)
❌ Poor Boundaries:
- User and Order Service (too coupled)
- Everything Service (just a monolith)
API Gateway Pattern
Copy
// gateway/app.js
const express = require('express');
const httpProxy = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const app = express();
// Rate limiting
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}));
// Authentication middleware
app.use('/api', authMiddleware);
// Route to services
app.use('/api/users', httpProxy.createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true,
pathRewrite: { '^/api/users': '' }
}));
app.use('/api/orders', httpProxy.createProxyMiddleware({
target: 'http://order-service:3002',
changeOrigin: true,
pathRewrite: { '^/api/orders': '' }
}));
app.use('/api/products', httpProxy.createProxyMiddleware({
target: 'http://product-service:3003',
changeOrigin: true,
pathRewrite: { '^/api/products': '' }
}));
app.listen(3000);
Service Communication
Synchronous (HTTP/REST)
Copy
// order-service calling user-service
const axios = require('axios');
class UserServiceClient {
constructor() {
this.baseURL = process.env.USER_SERVICE_URL || 'http://user-service:3001';
this.client = axios.create({
baseURL: this.baseURL,
timeout: 5000
});
}
async getUser(userId) {
try {
const { data } = await this.client.get(`/users/${userId}`);
return data;
} catch (error) {
if (error.response?.status === 404) {
return null;
}
throw new Error(`User service error: ${error.message}`);
}
}
async validateUser(userId) {
const user = await this.getUser(userId);
return user !== null;
}
}
module.exports = new UserServiceClient();
Circuit Breaker Pattern
Copy
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000, // Time before timeout
errorThresholdPercentage: 50, // % of failures before opening
resetTimeout: 30000 // Time before trying again
};
const breaker = new CircuitBreaker(async (userId) => {
const response = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
return response.data;
}, options);
breaker.on('open', () => console.log('Circuit breaker opened'));
breaker.on('halfOpen', () => console.log('Circuit breaker half-open'));
breaker.on('close', () => console.log('Circuit breaker closed'));
// Usage
const getUser = async (userId) => {
try {
return await breaker.fire(userId);
} catch (error) {
// Return cached data or default
return { id: userId, name: 'Unknown (service unavailable)' };
}
};
Asynchronous (Message Queue)
Copy
npm install amqplib
Copy
// RabbitMQ message producer
const amqp = require('amqplib');
class MessageQueue {
async connect() {
this.connection = await amqp.connect(process.env.RABBITMQ_URL);
this.channel = await this.connection.createChannel();
}
async publish(queue, message) {
await this.channel.assertQueue(queue, { durable: true });
this.channel.sendToQueue(
queue,
Buffer.from(JSON.stringify(message)),
{ persistent: true }
);
}
async subscribe(queue, handler) {
await this.channel.assertQueue(queue, { durable: true });
this.channel.consume(queue, async (msg) => {
try {
const content = JSON.parse(msg.content.toString());
await handler(content);
this.channel.ack(msg);
} catch (error) {
console.error('Message handling error:', error);
this.channel.nack(msg, false, false); // Dead letter
}
});
}
}
// Order service - publish event
const orderCreated = async (order) => {
await mq.publish('order.created', {
orderId: order.id,
userId: order.userId,
items: order.items,
total: order.total,
timestamp: new Date().toISOString()
});
};
// Notification service - consume event
mq.subscribe('order.created', async (event) => {
const user = await userService.getUser(event.userId);
await sendEmail(user.email, 'Order Confirmation', {
orderId: event.orderId,
total: event.total
});
});
// Inventory service - consume event
mq.subscribe('order.created', async (event) => {
for (const item of event.items) {
await decreaseStock(item.productId, item.quantity);
}
});
Event-Driven Architecture
Copy
// Event types
const EventTypes = {
USER_CREATED: 'user.created',
USER_UPDATED: 'user.updated',
ORDER_CREATED: 'order.created',
ORDER_PAID: 'order.paid',
ORDER_SHIPPED: 'order.shipped',
PAYMENT_RECEIVED: 'payment.received',
PAYMENT_FAILED: 'payment.failed'
};
// Event structure
const createEvent = (type, data, metadata = {}) => ({
id: uuid(),
type,
data,
metadata: {
timestamp: new Date().toISOString(),
version: '1.0',
source: process.env.SERVICE_NAME,
correlationId: metadata.correlationId || uuid(),
...metadata
}
});
// Publish event
await eventBus.publish(createEvent(EventTypes.ORDER_CREATED, {
orderId: order.id,
userId: order.userId,
items: order.items
}));
Service Discovery
With Docker Compose
Copy
# docker-compose.yml
version: '3.8'
services:
gateway:
build: ./gateway
ports:
- "3000:3000"
depends_on:
- user-service
- order-service
environment:
- USER_SERVICE_URL=http://user-service:3001
- ORDER_SERVICE_URL=http://order-service:3002
user-service:
build: ./user-service
environment:
- DATABASE_URL=mongodb://mongo:27017/users
depends_on:
- mongo
order-service:
build: ./order-service
environment:
- DATABASE_URL=postgres://postgres:password@postgres:5432/orders
- USER_SERVICE_URL=http://user-service:3001
- RABBITMQ_URL=amqp://rabbitmq:5672
depends_on:
- postgres
- rabbitmq
mongo:
image: mongo:6
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=orders
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672" # Management UI
With Consul (Production)
Copy
const Consul = require('consul');
const consul = new Consul({
host: process.env.CONSUL_HOST || 'localhost'
});
// Register service
const registerService = async () => {
await consul.agent.service.register({
name: 'user-service',
id: `user-service-${process.env.HOSTNAME}`,
address: process.env.HOST_IP,
port: parseInt(process.env.PORT),
check: {
http: `http://${process.env.HOST_IP}:${process.env.PORT}/health`,
interval: '10s'
}
});
};
// Discover service
const getServiceUrl = async (serviceName) => {
const services = await consul.catalog.service.nodes(serviceName);
if (services.length === 0) {
throw new Error(`Service ${serviceName} not found`);
}
// Simple round-robin
const service = services[Math.floor(Math.random() * services.length)];
return `http://${service.ServiceAddress}:${service.ServicePort}`;
};
Data Management
Database Per Service
Copy
// User service - MongoDB
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI);
const UserSchema = new mongoose.Schema({
email: { type: String, unique: true },
name: String,
createdAt: { type: Date, default: Date.now }
});
// Order service - PostgreSQL with Prisma
// prisma/schema.prisma
model Order {
id Int @id @default(autoincrement())
userId String // Reference to user in another service
items Json
total Decimal
status OrderStatus
createdAt DateTime @default(now())
}
// Product service - Redis for fast access
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);
const getProduct = async (id) => {
const cached = await client.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await fetchFromDB(id);
await client.setEx(`product:${id}`, 3600, JSON.stringify(product));
return product;
};
Saga Pattern for Distributed Transactions
Copy
// Choreography-based saga
class OrderSaga {
constructor(eventBus) {
this.eventBus = eventBus;
this.setupListeners();
}
setupListeners() {
// Step 1: Order created -> Reserve inventory
this.eventBus.subscribe('order.created', async (event) => {
try {
await this.reserveInventory(event.data);
await this.eventBus.publish('inventory.reserved', event.data);
} catch (error) {
await this.eventBus.publish('inventory.reservation.failed', event.data);
}
});
// Step 2: Inventory reserved -> Process payment
this.eventBus.subscribe('inventory.reserved', async (event) => {
try {
await this.processPayment(event.data);
await this.eventBus.publish('payment.completed', event.data);
} catch (error) {
// Compensate: release inventory
await this.releaseInventory(event.data);
await this.eventBus.publish('payment.failed', event.data);
}
});
// Step 3: Payment completed -> Confirm order
this.eventBus.subscribe('payment.completed', async (event) => {
await this.confirmOrder(event.data);
await this.eventBus.publish('order.confirmed', event.data);
});
// Compensation handlers
this.eventBus.subscribe('payment.failed', async (event) => {
await this.cancelOrder(event.data);
});
}
}
Health Checks and Monitoring
Copy
// Health check endpoint
app.get('/health', async (req, res) => {
const health = {
status: 'healthy',
service: process.env.SERVICE_NAME,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {}
};
// Database check
try {
await db.query('SELECT 1');
health.checks.database = { status: 'healthy' };
} catch (error) {
health.status = 'unhealthy';
health.checks.database = { status: 'unhealthy', error: error.message };
}
// Message queue check
try {
await mq.checkConnection();
health.checks.messageQueue = { status: 'healthy' };
} catch (error) {
health.status = 'unhealthy';
health.checks.messageQueue = { status: 'unhealthy', error: error.message };
}
// External service check
try {
await userService.ping();
health.checks.userService = { status: 'healthy' };
} catch (error) {
health.status = 'degraded'; // Might still work
health.checks.userService = { status: 'unhealthy', error: error.message };
}
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
});
Distributed Tracing
Copy
const { trace } = require('@opentelemetry/api');
// Trace an operation
const tracer = trace.getTracer('order-service');
app.post('/orders', async (req, res) => {
const span = tracer.startSpan('createOrder');
try {
span.setAttribute('userId', req.body.userId);
// Create child span for each operation
const userSpan = tracer.startSpan('validateUser', { parent: span });
const user = await userService.getUser(req.body.userId);
userSpan.end();
const orderSpan = tracer.startSpan('saveOrder', { parent: span });
const order = await Order.create(req.body);
orderSpan.end();
span.setAttribute('orderId', order.id);
res.json(order);
} catch (error) {
span.recordException(error);
span.setStatus({ code: 2, message: error.message });
throw error;
} finally {
span.end();
}
});
Summary
- Start with a monolith - Extract services when needed
- Define clear boundaries - One service, one responsibility
- Use API Gateway - Single entry point for clients
- Implement circuit breakers - Handle service failures gracefully
- Prefer async communication - Message queues reduce coupling
- Database per service - Avoid shared databases
- Handle distributed transactions - Use saga pattern
- Monitor everything - Health checks, tracing, logging
- Container orchestration - Use Kubernetes in production