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.