16. Microservices vs Monolith

The architectural choice between monolithic and microservice architectures fundamentally affects how a system is developed, deployed, scaled, and maintained.


Monolithic Architecture

All components of the application are built and deployed as a single unit.

┌──────────────────────────────────────────────┐
│              Monolithic Application           │
│                                              │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│  │  User    │ │  Order   │ │  Payment     │ │
│  │  Module  │ │  Module  │ │  Module      │ │
│  └──────────┘ └──────────┘ └──────────────┘ │
│  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│  │ Product  │ │  Search  │ │ Notification │ │
│  │  Module  │ │  Module  │ │  Module      │ │
│  └──────────┘ └──────────┘ └──────────────┘ │
│                                              │
│              Shared Database                 │
│              ┌──────────┐                    │
│              │    DB    │                    │
│              └──────────┘                    │
└──────────────────────────────────────────────┘

Advantages

Advantage Description
Simple development One codebase, one IDE, one build
Easy debugging Step through code in a single process
Simple deployment One artifact to deploy
No network overhead In-process function calls, not network calls
Transaction simplicity ACID transactions across all modules
Simple testing End-to-end testing in one process
Low operational overhead One thing to monitor, one thing to deploy

Disadvantages

Disadvantage Description
Scaling limitations Must scale entire app even if only one module needs it
Deployment risk One change requires redeploying everything
Technology lock-in All modules must use the same language/framework
Team coupling Large teams step on each other's code
Slow CI/CD Build and test times grow with codebase
Reliability One module's bug can crash the entire application
Code complexity Over time, module boundaries erode → "big ball of mud"

Microservices Architecture

The application is decomposed into small, independent services, each owning its own data and running in its own process.

┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│  User   │  │  Order  │  │ Payment │  │ Product │
│ Service │  │ Service │  │ Service │  │ Service │
│  [DB]   │  │  [DB]   │  │  [DB]   │  │  [DB]   │
└────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘
     │            │            │            │
     └────────────┴────────────┴────────────┘
                       │
              [Message Queue / API Gateway]
                       │
                   [Clients]

Advantages

Advantage Description
Independent deployment Deploy one service without touching others
Independent scaling Scale only the services that need it
Technology diversity Each service can use the best language/framework/DB
Team autonomy Small teams own individual services end-to-end
Fault isolation One service failure doesn't take down the system
Faster development Smaller codebases, faster builds
Organizational alignment Services align with business domains (Conway's Law)

Disadvantages

Disadvantage Description
Distributed complexity Network failures, latency, partial failures
Data consistency No ACID across services; must use eventual consistency, sagas
Operational overhead Many services to deploy, monitor, debug
Network latency Inter-service calls add latency
Testing complexity Integration/E2E testing across services is hard
Debugging difficulty Tracing a request across multiple services
Dependency management Service versions, API compatibility
Infrastructure cost More servers, containers, networking

Comparison

Aspect Monolith Microservices
Deployment Single unit Independent per service
Scaling Whole application Per service
Data management Shared database Database per service
Communication In-process function calls Network calls (HTTP, gRPC, messaging)
Transactions ACID (simple) Distributed (complex — Sagas)
Team size Works well for small teams Designed for large organizations
Tech stack Uniform Polyglot
Debugging Easy (single process) Hard (distributed tracing needed)
Startup speed Fast to build initially Slower initial setup
Long-term velocity Slows down as codebase grows Maintained if boundaries are clean

Service Communication Patterns

Synchronous Communication

Services call each other directly and wait for a response.

Order Service ──[HTTP/gRPC]──→ Payment Service ──→ Response
                                  │
                                  ├──→ Inventory Service ──→ Response
Protocol Use Case
REST (HTTP) Simple CRUD, public APIs
gRPC High-performance internal communication
GraphQL Aggregated API for clients

Risks: Cascading failures, tight coupling, latency accumulation.

Asynchronous Communication

Services communicate through message queues or event streams.

Order Service ──[Event: OrderCreated]──→ [Message Queue]
                                              │
                                    ├──→ Payment Service (subscribes)
                                    ├──→ Inventory Service (subscribes)
                                    └──→ Notification Service (subscribes)

Benefits: Decoupled, resilient to downstream failures, naturally scalable.


Key Microservice Patterns

API Gateway

Single entry point for all client requests.

Clients → [API Gateway] → Routes to appropriate service
                        → Handles auth, rate limiting, logging

Backend for Frontend (BFF)

Different API gateways for different clients.

Mobile App → [Mobile BFF] → Microservices
Web App    → [Web BFF]    → Microservices

Service Discovery

How services find each other.

Type Description Examples
Client-side Client queries a registry and selects a service instance Eureka, Consul (with client library)
Server-side Load balancer queries the registry on behalf of the client AWS ALB, Kubernetes Services
DNS-based Service names resolve via DNS Consul DNS, Kubernetes CoreDNS

Circuit Breaker

Prevents cascading failures by stopping calls to a failing service.

States:
CLOSED → calls pass through normally
  ↓ (failures exceed threshold)
OPEN → all calls fail immediately (fast failure, no waiting)
  ↓ (after timeout)
HALF-OPEN → allow a few test calls
  ↓ (if successful) → CLOSED
  ↓ (if still failing) → OPEN

Libraries: Resilience4j (Java), Polly (.NET), Hystrix (deprecated).

Saga Pattern

Distributed transactions across services.

Choreography (event-driven):
OrderCreated → [Queue] → PaymentProcessed → [Queue] → InventoryReserved → [Queue] → OrderConfirmed

Orchestration (central coordinator):
Saga Orchestrator → Create Order → Process Payment → Reserve Inventory → Confirm Order
                    (compensate:  Cancel Order ← Refund Payment ← Release Inventory)

Sidecar Pattern

Deploy auxiliary functionality alongside each service in a sidecar container.

[Pod: Order Service + Envoy Sidecar]
                |
[Pod: Payment Service + Envoy Sidecar]

The sidecar handles: logging, monitoring, service mesh, mTLS, retries.

Strangler Fig Pattern

Incrementally migrate from monolith to microservices.

Phase 1: All traffic → Monolith
Phase 2: New feature → Microservice; rest → Monolith
Phase 3: Next feature → Microservice; rest → Monolith
Phase N: All traffic → Microservices; Monolith decommissioned

Database per Service

Each microservice owns its database. Other services access data only through the service's API.

[User Service] → [User DB]          ← Only User Service can access
[Order Service] → [Order DB]        ← Only Order Service can access
[Product Service] → [Product DB]    ← Only Product Service can access

Challenge: How to query data across services?

Solution Description
API composition Query multiple services and join in the API layer
CQRS Separate read model (materialized view) built from events
Event sourcing Store events, build read models from event stream
Data lake Aggregate data in analytics store for reporting

Service Boundaries (How to Decompose)

Domain-Driven Design (DDD)

DDD Concept Application
Bounded Context Each service = one bounded context
Aggregate The unit of consistency within a service
Domain Events Inter-service communication
Ubiquitous Language Each service has its own vocabulary

Decomposition Strategies

Strategy Description
By business capability User management, order processing, billing
By subdomain Core, supporting, generic
By data ownership Who owns this data?
By team Conway's Law — architecture mirrors org structure

When Monolith, When Microservices?

Choose Monolith When:

  • Small team (< 10 developers).
  • Simple domain.
  • Early-stage product (prototype, MVP).
  • Well-understood, stable requirements.
  • Need simplicity over scalability.

Choose Microservices When:

  • Large team (multiple teams need autonomy).
  • Complex domain with clear boundaries.
  • Need independent scaling of components.
  • Need technology diversity.
  • Quick, independent deployment cycles matter.

The Evolution Path

Start: Monolith (fast development)
  ↓
Modular Monolith (clean internal boundaries)
  ↓
Extract hotspots to services (Strangler Fig)
  ↓
Full Microservices (when justified by scale/team)

"Don't start with microservices. Start with a monolith, keep it modular, and extract services when the pain of the monolith outweighs the complexity of distribution."


Summary

Concept Key Point
Monolith Simple, fast to start, hard to scale long-term
Microservices Complex, independent scaling and deployment
Communication Sync (REST/gRPC) for queries; Async (events) for commands
Circuit breaker Prevent cascading failures
Saga Distributed transactions via compensation
Database per service Each service owns its data
Strangler fig Incremental migration path

Rule of thumb: Start with a monolith. Extract services only when you have clear domain boundaries, team ownership, and scaling needs that justify the operational complexity.