Difficulty: Intermediate
When would you choose microservices over a monolith? What are the trade-offs and challenges of a microservices architecture?
Monolith: Single deployable unit containing all application logic. Simpler to develop, test, deploy, and debug. Best for small teams and early-stage products.
Microservices: Application split into small, independently deployable services, each owning its own data. Best for large teams, complex domains, and high-scale systems.
Key trade-offs: - Complexity: Monolith is simpler. Microservices add network complexity, distributed transactions, and operational overhead. - Deployment: Monolith deploys as one unit. Microservices allow independent deployments per service. - Scaling: Monolith scales entirely. Microservices scale individual services based on demand. - Team autonomy: Microservices let separate teams own separate services with different tech stacks.
Start with a monolith and extract microservices when clear domain boundaries emerge and team size demands it.
// MONOLITH: Single codebase, single deployment
// server/
// src/
// module/auth/ <- all modules in one app
// module/orders/
// module/payments/
// module/notifications/
// database/ <- single shared database
// index.ts <- single entry point
// MICROSERVICES: Separate services, separate deployments
// auth-service/ <- independent Node.js app
// src/index.ts
// database/ <- own database
// Dockerfile
// order-service/ <- independent app (could be Go, Python, etc.)
// src/index.ts
// database/ <- own database
// Dockerfile
// payment-service/
// src/index.ts
// database/
// Dockerfile
// api-gateway/ <- routes requests to services
// nginx.conf
// Communication:
// Synchronous: REST / gRPC between services
// Asynchronous: Message queue (Kafka/RabbitMQ) for events
// Example: Order placement flow
// 1. API Gateway -> Order Service (create order)
// 2. Order Service -> Payment Service (charge card) [sync/gRPC]
// 3. Order Service -> Kafka (ORDER_CREATED event)
// 4. Inventory Service consumes event (reserve stock)
// 5. Notification Service consumes event (send email)
Microservices separate concerns into independently deployable units. Each service owns its data and communicates via APIs or events. The API gateway provides a single entry point for clients.
// API Gateway routes requests to appropriate services
// Also handles: auth, rate limiting, request aggregation
// Kong / AWS API Gateway / custom Node.js gateway
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
// Auth middleware applied globally
app.use(authMiddleware);
app.use(rateLimitMiddleware);
// Route to services
app.use('/api/auth', proxy({
target: 'http://auth-service:3001',
pathRewrite: { '^/api/auth': '' }
}));
app.use('/api/orders', proxy({
target: 'http://order-service:3002',
pathRewrite: { '^/api/orders': '' }
}));
app.use('/api/payments', proxy({
target: 'http://payment-service:3003',
pathRewrite: { '^/api/payments': '' }
}));
// Request aggregation (Backend for Frontend)
app.get('/api/dashboard', async (req, res) => {
// Aggregate data from multiple services
const [orders, payments, notifications] = await Promise.all([
fetch('http://order-service:3002/recent'),
fetch('http://payment-service:3003/summary'),
fetch('http://notification-service:3004/unread')
]);
res.json({ orders, payments, notifications });
});
The API gateway is the single entry point for all client requests. It handles cross-cutting concerns like auth and rate limiting, and can aggregate responses from multiple services into a single response.
// Strangler Fig: gradually extract microservices
// from monolith without a full rewrite
// Phase 1: Identify bounded contexts
// - Auth (users, sessions, OAuth)
// - Orders (cart, checkout, order history)
// - Payments (billing, subscriptions)
// - Notifications (email, push, SMS)
// Phase 2: Add API Gateway in front of monolith
// All traffic: Client -> Gateway -> Monolith
// Phase 3: Extract one service at a time
// New: Client -> Gateway -> Auth Service (extracted)
// Old: Client -> Gateway -> Monolith (orders, payments, notif)
// Phase 4: Data migration
// 1. New service reads/writes to own database
// 2. Sync data from monolith DB during transition
// 3. Cut over when new service is stable
// Phase 5: Repeat until monolith is empty
// Key rules:
// - Extract the least coupled module first
// - Keep both old and new running simultaneously
// - Use feature flags to gradually route traffic
// - Never do a big-bang migration
// Typical extraction order:
// 1. Auth (least dependencies)
// 2. Notifications (fire-and-forget)
// 3. Payments (clear boundary)
// 4. Core domain last (most coupled)
The Strangler Fig pattern lets you incrementally replace a monolith with microservices. Extract one service at a time, route traffic gradually, and keep both systems running during the transition.
Microservices, Monolith, API Gateway, Service Communication, Domain-Driven Design