Introduction — The Most Debated Architectural Decision
"Should we use microservices?" is the question that launched a thousand conference talks, blog posts, and architectural arguments. For nearly a decade, "microservices" was synonymous with "modern" and "monolith" was synonymous with "legacy." But the industry is waking up to a more nuanced reality: architecture is a tradeoff, not a progression.
This article examines monoliths, modular monoliths, and microservices through the lens that matters most for this series: delivery. How does each architecture affect your ability to build, test, deploy, and operate software? What does each demand from your CI/CD pipelines, team structure, and observability?
The Pendulum Swings Back
The industry has gone through a clear cycle:
- 2005-2012: Large monoliths dominate. Companies like Amazon, Netflix, and eBay struggle with deployment speed.
- 2012-2018: Microservices revolution. Netflix, Uber, and Spotify evangelise the approach. Everyone wants to "do microservices."
- 2018-2023: Reality hits. Teams discover the "distributed systems tax." Many find they traded monolith problems for worse distributed problems.
- 2023-present: Nuance prevails. "Modular monolith" emerges as a middle ground. Amazon Prime Video famously moves back from microservices to a monolith for one service, reducing costs by 90%.
The Monolith
A monolith is a single deployable unit containing all application functionality. One codebase, one build process, one deployment artifact. When you deploy, you deploy everything.
Types of Monoliths
| Type | Description | Internal Structure | Example |
|---|---|---|---|
| Big Ball of Mud | No internal structure. Everything calls everything. | Spaghetti code, no boundaries | Legacy enterprise apps |
| Layered Monolith | Traditional MVC or N-tier architecture | Horizontal layers (UI, service, data) | Most Spring Boot / Rails apps |
| Modular Monolith | Well-defined internal modules with clear boundaries | Vertical slices by domain | Shopify, Basecamp |
When Monoliths Win
Monoliths have significant advantages that are often overlooked in the microservices hype:
- Simple deployment — One artifact, one pipeline, one deploy. No service coordination required.
- Easy debugging — Stack traces show the full call path. No distributed tracing needed.
- No network calls between components — In-process function calls are millions of times faster than HTTP/gRPC.
- Simple transactions — ACID transactions across the entire application. No saga patterns needed.
- Lower operational overhead — One service to monitor, one set of logs, one scaling strategy.
- Faster development for small teams — No service contracts, no API versioning, no service discovery.
# Monolith deployment: one command
docker build -t myapp:v2.3.1 .
docker push registry.example.com/myapp:v2.3.1
kubectl set image deployment/myapp myapp=registry.example.com/myapp:v2.3.1
# That's it. One image, one deployment, one rollback target.
Amazon Started as a Monolith
Amazon.com ran as a monolithic C++ application called "Obidos" for years. It handled product catalogue, shopping cart, checkout, recommendations — everything. The monolith only became a problem when Amazon grew to hundreds of developers and needed independent deployment. They decomposed into services because of team scale, not because monoliths are inherently bad. The lesson: start with a monolith, decompose when the pain of coordination exceeds the pain of distribution.
The Modular Monolith
A modular monolith is a single deployable unit with well-defined internal module boundaries. Each module owns a specific domain, has a clear public API, and cannot access the internals of other modules. Think of it as microservices within a single process.
# Modular monolith structure
src/
├── modules/
│ ├── orders/
│ │ ├── api/ # Public interface (what other modules can call)
│ │ ├── domain/ # Business logic (private)
│ │ ├── persistence/ # Database access (private)
│ │ └── events/ # Domain events published
│ ├── payments/
│ │ ├── api/
│ │ ├── domain/
│ │ ├── persistence/
│ │ └── events/
│ ├── inventory/
│ │ ├── api/
│ │ ├── domain/
│ │ ├── persistence/
│ │ └── events/
│ └── shipping/
│ ├── api/
│ ├── domain/
│ ├── persistence/
│ └── events/
├── shared/ # Shared kernel (minimal)
└── main.py # Single entry point
The key rules of a modular monolith:
- Modules communicate only through public APIs — no reaching into another module's database tables or internal classes.
- Each module owns its data — separate schemas or separate tables with no cross-module joins.
- Modules publish domain events — async communication between modules via an in-process event bus.
- Compilation/linting enforces boundaries — tools like ArchUnit (Java), Packwerk (Ruby), or custom lint rules prevent violations.
Shopify's Modular Monolith
Shopify is the most prominent example of a modular monolith at scale. Their Ruby on Rails monolith serves millions of merchants and processes billions in transactions. Rather than decomposing into microservices, they invested in Packwerk — a tool that enforces module boundaries at the code level.
Microservices
Microservices are independently deployable services, each owning a specific business capability. Each service has its own codebase, its own deployment pipeline, its own data store, and can be written in different programming languages.
Benefits When Done Right
- Independent deployment — Change one service without touching others. Deploy 50 times a day if needed.
- Independent scaling — Scale the payment service to 100 instances while keeping the admin panel at 2.
- Technology diversity — Use Python for ML services, Go for high-performance APIs, Node.js for real-time features.
- Fault isolation — If the recommendation service crashes, checkout still works.
- Team autonomy — Teams own services end-to-end. No coordinated releases needed.
The Distributed Systems Tax
Every benefit of microservices comes with a corresponding cost — the "distributed systems tax" you pay regardless of whether you need the benefit:
| Benefit You Get | Tax You Pay |
|---|---|
| Independent deployment | API versioning, contract testing, backward compatibility |
| Independent scaling | Service discovery, load balancing, connection pooling |
| Technology diversity | Multiple build pipelines, varied expertise needed |
| Fault isolation | Circuit breakers, retries, timeouts, bulkheads |
| Team autonomy | Cross-service debugging, distributed tracing, data consistency |
Distributed Systems Challenges
The Eight Fallacies of Distributed Computing
In 1994, Peter Deutsch identified eight assumptions that developers make about networks — all of which are false:
- The network is reliable — Packets get lost. Connections drop. Cables get cut.
- Latency is zero — Every network call adds milliseconds. 50 service calls = 50× the latency.
- Bandwidth is infinite — Serialisation overhead, payload sizes, and congestion are real.
- The network is secure — Every service-to-service call is an attack surface.
- Topology does not change — Services move, IPs change, instances scale up and down.
- There is one administrator — Different teams own different services with different policies.
- Transport cost is zero — Serialisation/deserialisation, TLS handshakes, DNS lookups all cost CPU.
- The network is homogeneous — Different protocols, versions, and configurations across services.
The Saga Pattern — Distributed Transactions
In a monolith, you wrap a business operation in a database transaction: either everything succeeds or everything rolls back. In microservices, this is impossible — each service owns its own database. The Saga pattern provides eventual consistency through a series of local transactions with compensating actions:
sequenceDiagram
participant O as Order Service
participant P as Payment Service
participant I as Inventory Service
participant S as Shipping Service
O->>O: Create Order (PENDING)
O->>P: Reserve Payment
P-->>O: Payment Reserved
O->>I: Reserve Inventory
I-->>O: Inventory Reserved
O->>S: Schedule Shipping
S-->>O: Shipping Scheduled
O->>O: Mark Order CONFIRMED
Note over O,S: If any step fails...
O->>S: Cancel Shipping (compensate)
O->>I: Release Inventory (compensate)
O->>P: Release Payment (compensate)
O->>O: Mark Order FAILED
Conway's Law
"Any organisation that designs a system will produce a design whose structure is a copy of the organisation's communication structure." — Melvin Conway, 1967
This is not just an observation — it is a force of nature in software engineering. If you have three teams, you will end up with three services (or three modules) regardless of what the architecture diagram says. Conway's Law has profound implications for architecture decisions:
- Small team (5-8 people)? → A monolith matches your communication structure perfectly. You all talk to each other daily anyway.
- Multiple teams (20+ people)? → Microservices align with team boundaries. Each team owns a service.
- Inverse Conway Maneuver → Deliberately structure your teams to produce the architecture you want. Want three microservices? Create three teams.
Service Mesh
A service mesh is an infrastructure layer for service-to-service communication. It provides traffic management, security (mTLS), and observability without requiring changes to application code. The mesh works by deploying a sidecar proxy alongside each service instance.
flowchart LR
subgraph Pod A
A[Service A] --> PA[Proxy A]
end
subgraph Pod B
PB[Proxy B] --> B[Service B]
end
subgraph Pod C
PC[Proxy C] --> C[Service C]
end
subgraph Control Plane
CP[Mesh Controller]
end
PA -->|mTLS| PB
PA -->|mTLS| PC
CP -->|Config| PA
CP -->|Config| PB
CP -->|Config| PC
Major service mesh implementations:
| Mesh | Sidecar | Key Features | Complexity |
|---|---|---|---|
| Istio | Envoy | Full-featured, traffic management, canary deployments | High |
| Linkerd | linkerd2-proxy (Rust) | Lightweight, fast, simple to operate | Low |
| Consul Connect | Envoy | Multi-platform, service discovery built-in | Medium |
Decomposition Strategies
When you decide to break a monolith apart, how you decompose matters enormously. Do it wrong and you get a "distributed monolith" — the worst of both worlds.
The Strangler Fig Pattern
Named after strangler fig trees that gradually envelop their host, this pattern lets you incrementally replace monolith functionality with new services:
flowchart TD
subgraph Phase 1
LB1[Load Balancer] --> M1[Monolith
Handles Everything]
end
subgraph Phase 2
LB2[Load Balancer] --> M2[Monolith
Most Features]
LB2 --> S1[New Service
Orders]
end
subgraph Phase 3
LB3[Load Balancer] --> M3[Monolith
Legacy Only]
LB3 --> S2[Orders Service]
LB3 --> S3[Payments Service]
LB3 --> S4[Users Service]
end
Key decomposition strategies:
- Strangler Fig — Route traffic to new service for specific paths. Gradually move functionality until the monolith is empty.
- Domain-Driven Decomposition — Identify bounded contexts using DDD. Each bounded context becomes a service candidate.
- Event Storming — Workshop technique to discover domain events, aggregates, and bounded contexts. Excellent for finding natural service boundaries.
- Database Seam — Find tables that are accessed by only one part of the codebase. Extract that code and those tables into a service.
Segment's Return to the Monolith
Segment (now part of Twilio) famously moved from microservices back to a monolith in 2018. With 3 engineers maintaining 140+ microservices, the operational overhead was crushing. Each service needed its own CI pipeline, monitoring, on-call rotation, and dependency management. By consolidating back to a monolith, they reduced operational load by 80% and improved developer productivity. Their blog post "Goodbye Microservices: From 100s of Problem Children to 1 Superstar" became one of the most-shared engineering posts of the year.
Delivery Implications
Your architecture fundamentally shapes your delivery process. Here is a direct comparison:
| Aspect | Monolith | Microservices |
|---|---|---|
| CI Pipeline | One pipeline, one build | N pipelines (one per service) |
| Build Time | Grows with codebase (can get long) | Small per service (but N builds total) |
| Testing | Full integration test suite runs every deploy | Contract tests + service-level tests + E2E |
| Deployment | All-or-nothing (entire app) | Independent per service |
| Rollback | Rollback entire application | Rollback individual service |
| Release Coordination | Everyone coordinates on release day | Teams release independently |
| API Versioning | Not needed (in-process calls) | Critical (breaking changes break callers) |
| Observability | Simple (one set of logs/metrics) | Complex (distributed tracing required) |
| Incident Debugging | Single stack trace | Trace across N services |
When to Choose What
There is no universal "best" architecture. The right choice depends on your context:
flowchart TD
A[New Project?] --> B{Team Size?}
B -->|1-8 engineers| C{Product Maturity?}
B -->|8-30 engineers| D{Clear Domain Boundaries?}
B -->|30+ engineers| E[Microservices likely appropriate]
C -->|Early/MVP| F[Start with Monolith]
C -->|Mature/Scaling| G[Modular Monolith]
D -->|Yes| H{CI/CD + Observability mature?}
D -->|No| G
H -->|Yes| I[Consider Microservices]
H -->|No| G
F --> J[Evolve when pain appears]
G --> K[Extract services only when justified]
| Choose This | When |
|---|---|
| Monolith | Small team, new product, unclear domain boundaries, speed of iteration is priority |
| Modular Monolith | Growing team (8-20), clear domains, want team ownership without operational complexity |
| Microservices | Large org (30+), mature CI/CD, strong observability, teams need independent deployment, different scaling requirements per component |
Exercises
Audit Your Current Architecture
Draw the architecture of a system you work with (or a well-known open-source project). Classify it: big ball of mud, layered monolith, modular monolith, or microservices. Identify: How many deployable units exist? How many teams contribute? What is the deployment frequency? Would a different architecture improve delivery speed?
Design Module Boundaries
Take an e-commerce application (products, orders, payments, shipping, users, reviews). Design it as a modular monolith: define module boundaries, public APIs between modules, which module owns which database tables, and how modules communicate (sync calls vs domain events). Draw the dependency graph — are there circular dependencies?
Apply Conway's Law
You have 4 teams of 5 engineers each. Map them to either: (a) a monolith with each team owning specific modules, or (b) microservices with each team owning 2-3 services. For each option, describe: the deployment process, how cross-cutting changes work, and what happens when an incident spans team boundaries.
Plan a Strangler Fig Migration
You have a monolithic order management system that needs to extract the "payments" capability into its own service. Write a 6-step migration plan using the Strangler Fig pattern: what to extract first, how to route traffic during migration, how to handle the shared database, and how to verify correctness during the transition.
Conclusion & Next Steps
The monolith vs microservices debate is not about which is "better" — it is about which fits your context. A monolith is not legacy; microservices are not modern. Both are tools with tradeoffs. The modular monolith offers a compelling middle ground that gives you module boundaries and team ownership without the operational tax of distribution.
The most important takeaway: architecture should follow team structure and delivery capability. If you do not have CI/CD maturity, observability, and team autonomy, microservices will make everything harder, not easier. Build the delivery foundations first (Parts 1-26 of this series), then let your architecture evolve to match your growing capabilities.
In Part 28: Release Architecture & Multi-Environment Systems, we will explore how software moves through multiple environments — from developer laptops through staging to production — and how to design promotion pipelines, manage environment parity, and handle configuration across environments.