Modern Backend Architectures: Microservices vs. Monoliths
Every developer eventually faces this decision. You are starting a new project — or scaling an existing one — and someone in the room says "we should do microservices." Someone else says "let's keep it simple." Both are right, depending on context.
This post cuts through the hype and gives you a practical framework for making the right call.
"Don't start with microservices. Start with a monolith, identify the seams, then extract services where it actually makes sense." — Martin Fowler
1. The Monolithic Architecture
A monolith is a single deployable unit. All your business logic, data access, and API handling live in one codebase and deploy together.
text┌─────────────────────────────────────────┐ │ Monolithic App │ │ │ │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ │ │ Auth │ │ Products │ │Orders │ │ │ └──────────┘ └──────────┘ └───────┘ │ │ │ │ ┌─────────────────────────────────────┐│ │ │ Single Database ││ │ └─────────────────────────────────────┘│ └─────────────────────────────────────────┘
A Clean Monolith in Node.js
text// src/app.ts — everything in one deployable unit import express from 'express'; import { authRouter } from './routes/auth'; import { productsRouter } from './routes/products'; import { ordersRouter } from './routes/orders'; import { db } from './lib/database'; const app = express(); app.use(express.json()); // All routes in one process app.use('/api/auth', authRouter); app.use('/api/products', productsRouter); app.use('/api/orders', ordersRouter); app.listen(3000, () => console.log('Server running on port 3000'));
When Monoliths Win
- Early-stage startups (ship fast, iterate faster)
- Small teams (2-5 developers)
- Unclear domain boundaries
- Simple deployment requirements
- When you need to move fast without DevOps overhead
2. The Microservices Architecture
Microservices decompose the application into small, independently deployable services. Each service owns its data and communicates over a network.
text┌──────────┐ ┌──────────┐ ┌──────────┐ │ Auth │ │ Products │ │ Orders │ │ Service │ │ Service │ │ Service │ │ │ │ │ │ │ │ ┌────┐ │ │ ┌────┐ │ │ ┌────┐ │ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │ │ └────┘ │ │ └────┘ │ │ └────┘ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ └───────────────┴───────────────┘ │ ┌──────┴──────┐ │ API Gateway │ └─────────────┘
Service Communication Patterns
text// Synchronous: REST or gRPC (request/response) // Use when: you need an immediate response // orders-service/src/handlers/createOrder.ts async function createOrder(userId: string, items: CartItem[]) { // Call products service to verify stock const stockCheck = await fetch(`http://products-service/api/stock/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }), }); if (!stockCheck.ok) throw new Error('Items out of stock'); // Call auth service to verify user const user = await fetch(`http://auth-service/api/users/${userId}`); if (!user.ok) throw new Error('User not found'); // Create order in own database return await db.orders.create({ userId, items, status: 'pending' }); }
text// Asynchronous: Message Queue (event-driven) // Use when: you don't need an immediate response // After order is created, publish an event import { kafka } from './lib/kafka'; async function publishOrderCreated(order: Order) { await kafka.producer.send({ topic: 'order.created', messages: [{ key: order.id, value: JSON.stringify({ orderId: order.id, userId: order.userId, items: order.items, timestamp: new Date().toISOString(), }), }], }); } // Inventory service listens and decrements stock // Email service listens and sends confirmation // Analytics service listens and records the event // All independently, without coupling
3. The Real Comparison
| Factor | Monolith | Microservices |
|---|---|---|
| Initial complexity | Low | High |
| Deployment | Single unit | Per-service CI/CD |
| Scaling | Scale everything | Scale individual services |
| Team size | Small (1-10) | Large (10+) |
| Data consistency | Easy (ACID transactions) | Hard (eventual consistency) |
| Debugging | Easy (single process) | Hard (distributed tracing needed) |
| Tech diversity | One stack | Multiple stacks possible |
| Operational overhead | Low | High (K8s, service mesh, etc.) |
4. Containerizing Either Architecture
Docker works for both. The difference is how many containers you run.
text# Dockerfile for a single service (works for monolith too) FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM node:20-alpine AS runner WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules EXPOSE 3000 CMD ["node", "dist/app.js"]
text# docker-compose.yml for microservices local dev version: '3.8' services: api-gateway: build: ./api-gateway ports: ["3000:3000"] depends_on: [auth-service, products-service, orders-service] auth-service: build: ./auth-service environment: DATABASE_URL: postgres://auth-db:5432/auth products-service: build: ./products-service environment: DATABASE_URL: postgres://products-db:5432/products orders-service: build: ./orders-service environment: DATABASE_URL: postgres://orders-db:5432/orders KAFKA_BROKER: kafka:9092 kafka: image: confluentinc/cp-kafka:latest environment: KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
5. The Strangler Fig Pattern: Migrating Safely
If you have a monolith that needs to become microservices, do not rewrite it all at once. Use the Strangler Fig pattern — extract services incrementally.
textPhase 1: Monolith handles everything ┌─────────────────────────────┐ │ Monolith │ │ Auth + Products + Orders │ └─────────────────────────────┘ Phase 2: Extract Auth service, route via gateway ┌──────────┐ ┌─────────────────────┐ │ Auth │ │ Monolith │ │ Service │ │ Products + Orders │ └──────────┘ └─────────────────────┘ Phase 3: Extract Products, monolith shrinks ┌──────────┐ ┌──────────┐ ┌─────────┐ │ Auth │ │ Products │ │Monolith │ │ Service │ │ Service │ │ Orders │ └──────────┘ └──────────┘ └─────────┘
6. System Design Deep Dive

7. Decision Framework
Ask yourself these questions before choosing:
Final Thoughts
The microservices vs. monolith debate is a false dichotomy. The real question is: what level of complexity does your team and product actually need right now?
Netflix, Uber, and Amazon use microservices because they have thousands of engineers and genuinely need independent scaling of hundreds of services. You probably do not. Start simple, measure where the pain is, and evolve your architecture based on real constraints — not architectural fashion.

Vighnesh Salunkhe
"Passionate about building scalable web applications and exploring the intersection of AI and human creativity."
Join the Conversation
Share your thoughts or ask a question