Microservices vs Monolith

Difficulty: Intermediate

Question

When would you choose microservices over a monolith? What are the trade-offs and challenges of a microservices architecture?

Answer

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.

Code examples

Monolith vs Microservices Structure

// 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 Pattern

// 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.

Migration Strategy: Strangler Fig Pattern

// 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.

Key points

Concepts covered

Microservices, Monolith, API Gateway, Service Communication, Domain-Driven Design