Monolith vs microservices
In this series (12 parts)
- Monolith vs microservices
- Microservice communication patterns
- Service discovery and registration
- Event-driven architecture
- Distributed data patterns
- Caching architecture patterns
- Search architecture
- Storage systems at scale
- Notification systems
- Real-time systems architecture
- Batch and stream processing
- Multi-region and global systems
Every system starts as a monolith, whether you plan it that way or not. The first commit is a single deployable unit, and that is perfectly fine. The question is never “monolith or microservices?” at day one. The real question is: at what point does the monolith’s coupling cost exceed the coordination cost of distribution?
What a monolith actually is
A monolith is a single deployment unit where all application logic runs in one process. The web layer, business logic, and data access all share the same memory space and the same release cycle. This is not inherently bad. Many successful products, Basecamp and Stack Overflow among them, run on well-structured monoliths that serve millions of users.
graph TD
subgraph Monolith["Monolith (single process)"]
UI["Web Layer"]
BL["Business Logic"]
DA["Data Access"]
UI --> BL --> DA
end
DA --> DB[("Database")]
Client["Client"] --> UI
A typical monolith: all layers share one process and one deployment.
The monolith gives you things that distributed systems must fight hard to achieve: strong consistency through local transactions, simple debugging with a single stack trace, straightforward deployment with one artifact. Latency between components is a function call, measured in nanoseconds rather than the milliseconds of a network hop.
The pain points that trigger change
Monoliths start hurting when the team grows. Two engineers can coordinate changes to shared code easily. Twenty engineers stepping on each other in the same codebase creates merge conflicts, broken builds, and a deployment queue that stretches to Friday.
The symptoms are recognizable: deploy cycles slow to weeks because one team’s feature blocks another’s bugfix. A memory leak in the reporting module takes down the checkout flow. Schema migrations require coordinating across six teams. The test suite takes forty minutes and nobody runs it locally anymore.
Notice the crossover. For small teams, the monolith wins on deployment speed because there is no coordination overhead. As teams grow, independent deployability becomes the dominant factor.
Conway’s Law is not optional
Melvin Conway observed in 1967 that organizations produce designs that mirror their communication structures. This is not a suggestion; it is a force of nature. If you have four teams, you will get four services (or four modules, or four layers). Fighting Conway’s Law is fighting gravity.
This means the architecture decision is fundamentally a team topology decision. Microservices work when you can draw clear ownership boundaries: Team A owns the order service, Team B owns inventory, Team C owns payments. Each team can deploy independently, choose their own tech stack within guardrails, and scale their service based on its specific load profile.
graph LR
subgraph Team_A["Team A"]
OS["Order Service"]
end
subgraph Team_B["Team B"]
IS["Inventory Service"]
end
subgraph Team_C["Team C"]
PS["Payment Service"]
end
subgraph Team_D["Team D"]
NS["Notification Service"]
end
OS -->|"gRPC"| IS
OS -->|"REST"| PS
PS -->|"Event"| NS
OS -->|"Event"| NS
Microservice topology aligned with team ownership. Each team owns, deploys, and operates its service independently.
If you cannot draw these boundaries cleanly, microservices will create more problems than they solve. Two teams sharing one service is painful. One team owning six services is unsustainable.
The cost of distribution
Splitting a monolith into services introduces an entire category of problems that did not exist before. Network calls fail. Messages get lost or delivered twice. Distributed transactions require complex coordination patterns like the saga pattern. Debugging a request that spans five services requires distributed tracing infrastructure.
Here is a partial list of what you now need:
Service discovery so services can find each other. Load balancing to distribute traffic. Circuit breakers to handle downstream failures gracefully. An API gateway to present a unified interface to clients. A message queue for asynchronous communication. Centralized logging and observability because you can no longer grep one log file.
Each of these introduces operational complexity and requires engineering investment. A team of five building microservices will spend more time on infrastructure than on product features.
The well-structured monolith
Before reaching for microservices, consider the modular monolith. This approach keeps the single deployment unit but enforces strong module boundaries internally. Each module has a public API, hides its internals, and owns its database tables. Modules communicate through well-defined interfaces, not by reaching into each other’s database tables.
graph TD
subgraph Monolith["Modular Monolith"]
subgraph Orders["Orders Module"]
OA["Orders API"]
OI["Orders Internal"]
end
subgraph Inventory["Inventory Module"]
IA["Inventory API"]
II["Inventory Internal"]
end
subgraph Payments["Payments Module"]
PA["Payments API"]
PI["Payments Internal"]
end
OA -->|"internal call"| IA
OA -->|"internal call"| PA
end
DB1[("Orders DB schema")]
DB2[("Inventory DB schema")]
DB3[("Payments DB schema")]
OI --> DB1
II --> DB2
PI --> DB3
A modular monolith enforces service-like boundaries while keeping deployment simple.
The modular monolith gives you the ability to extract services later when you actually need independent scaling or deployment. It is much easier to split a well-structured monolith than to untangle a distributed mess.
The strangler fig pattern
When you do decide to extract services, the strangler fig pattern provides a safe migration path. Named after the strangler fig tree that gradually envelops its host, this pattern routes traffic incrementally from the monolith to a new service.
graph LR
Client["Client"] --> Proxy["API Gateway / Proxy"]
Proxy -->|"/orders/*"| NewSvc["New Order Service"]
Proxy -->|"everything else"| Mono["Monolith"]
NewSvc --> NewDB[("Orders DB")]
Mono --> OldDB[("Monolith DB")]
NewSvc -.->|"sync during migration"| OldDB
Strangler fig in progress: the proxy routes order traffic to the new service while everything else stays in the monolith.
The process works in stages. First, you put a proxy (often an API gateway) in front of the monolith. Then you build the new service alongside the monolith. You redirect traffic for specific routes to the new service. You keep both running in parallel while validating behavior. Once confident, you remove the old code from the monolith.
The critical advantage is that you can stop at any point. If the extraction is not going well, the monolith still handles the traffic. There is no big-bang cutover, no weekend migration with everyone on call.
Decision framework
The choice between monolith and microservices is not about technology preferences. It is about organizational context, team maturity, and the specific problems you face.
Stay with a monolith when your team is small (under 10-15 engineers), when you are still discovering your domain boundaries, when you need to move fast on product features, or when your operational maturity does not yet support distributed systems.
Move toward microservices when independent deployment is a hard requirement, when different components have drastically different scaling needs, when team autonomy matters more than simplicity, and when you have the infrastructure investment to support it.
The worst outcome is a distributed monolith: microservices that are so tightly coupled they must be deployed together, giving you all the complexity of distribution with none of the benefits of independence. If your services cannot be deployed independently, you have a distributed monolith, and you should seriously consider merging them back.
What comes next
With the monolith vs microservices decision framed, the next question becomes: how do services actually talk to each other? The answer involves synchronous calls, asynchronous events, and patterns like sagas for distributed transactions. Continue with microservice communication patterns.