Modern Backend Architectures: Microservices vs. Monoliths
Architecture6 min read

Modern Backend Architectures: Microservices vs. Monoliths

Vighnesh Salunkhe

Vighnesh Salunkhe

Full Stack Developer

Published

April 24, 2026

Modern Backend Architectures: Microservices vs. Monoliths

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
Shopify, Stack Overflow, and Basecamp all run on monoliths at massive scale. A well-structured monolith can handle millions of users. Do not let anyone tell you otherwise.

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
Event-driven communication is the key to truly decoupled microservices. If Service A needs to call Service B synchronously for every request, you have not really decoupled them — you have just added network latency.

3. The Real Comparison

FactorMonolithMicroservices
Initial complexityLowHigh
DeploymentSingle unitPer-service CI/CD
ScalingScale everythingScale individual services
Team sizeSmall (1-10)Large (10+)
Data consistencyEasy (ACID transactions)Hard (eventual consistency)
DebuggingEasy (single process)Hard (distributed tracing needed)
Tech diversityOne stackMultiple stacks possible
Operational overheadLowHigh (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
Running microservices locally with Docker Compose is manageable. Running them in production requires Kubernetes, a service mesh (Istio/Linkerd), distributed tracing (Jaeger/Zipkin), and centralized logging (ELK stack). Budget significant DevOps time before committing.

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.

text
Phase 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

Video thumbnail
Watch on YouTube

7. Decision Framework

Ask yourself these questions before choosing:

Team size under 10? Start with a monolith. The coordination overhead of microservices will slow you down more than the architecture helps.
Clear domain boundaries? If you cannot draw clean lines between your business domains, microservices will create a distributed monolith — the worst of both worlds.
Need independent scaling? If your image processing service needs 10x the resources of your auth service, microservices make sense. If everything scales together, a monolith is simpler.
The right answer for most teams: Start with a well-structured modular monolith. Use clear module boundaries. When a specific module needs independent scaling or a different team owns it, extract it into a service.

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.

#Backend#Microservices#Docker#System Design
Vighnesh Salunkhe
Written by

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

Share this article