Monolith vs Microservices vs Modular Monolith
Every new project faces the same question: one deployable application or separately deployed services? A monolith minimizes distributed-systems overhead; microservices can enable independent ownership and scaling but add network failures, tracing, and release coordination. A modular monolith keeps one deployment while enforcing internal boundaries that may later support extraction.
Company stage and headcount do not decide this mechanically. Choose from measured scaling bottlenecks, release coupling, ownership boundaries, reliability requirements, and operational capability. This article compares the three styles and gives evidence you can collect before splitting.
If services are justified, use the microservices design patterns to shape their boundaries and failure handling. Choose each synchronous or asynchronous hop with the service communication protocol map.
Monolith
A monolith is a single deployment and often one process and database. Features use in-process calls, which simplifies local development, transactions, and refactoring. It is often efficient while independent deployability is not worth its operating cost.
Pressure appears when evidence shows shared failure domains, release queues, or resource contention. One module can affect the process, and coarse scaling replicates the whole unit. Those are possible pressures, not inevitable outcomes: modular boundaries and safer deployment techniques can extend a monolith's useful life.
| Pattern | Use when | Main trade-off |
|---|---|---|
| Monolith | One release and failure domain are acceptable | Simple operations; coarse scaling and deployment |
| Modular monolith | Domain boundaries matter before independent deploys do | Boundaries need enforcement; still one runtime unit |
| Microservices | A boundary needs independent ownership, scaling, or isolation | Network failure, data contracts, and multi-service operations |
Quick reference
- Good fit when one deployment cadence and failure domain are acceptable and the workload scales together.
- Strengths: simple deployment, easy debugging, fast local development, no network latency between modules.
- Weaknesses: scaling is all-or-nothing, deployment risk grows, team coordination overhead, tight coupling.
- Keep modules in separate packages even inside a monolith to prepare for future extraction.
- Use feature flags and canary deploys to reduce deployment risk as the monolith grows.
Remember this
Start with a monolith when speed and simplicity matter more than independent scaling.
Modular Monolith
A modular monolith keeps one deployment but enforces bounded contexts inside the codebase. The Users module owns its tables and exposes a public interface — other modules call UsersService.create(), not UsersRepository directly. Cross-module communication goes through well-defined APIs, not shared database joins.
It is a strong candidate before splitting because it tests whether boundaries are real without paying network and multi-deployment costs. Extraction is still a migration: an in-process interface does not automatically solve remote latency, partial failure, data ownership, or backward-compatible contracts.
Quick reference
- Good fit when domain boundaries are clear but independent deployment or scaling has not earned its cost.
- Strengths: one deploy, enforced module boundaries, easier extraction path, no distributed systems overhead.
- Weaknesses: requires discipline — teams must resist cross-module shortcuts, still one scaling unit.
- Use package-level boundaries (e.g. /modules/users, /modules/orders) with lint rules blocking cross-imports.
- Each module owns its database schema — no shared tables between domains.
- Communicate between modules via in-process events or explicit service interfaces.
Remember this
Choose a modular monolith when enforceable boundaries solve today's coupling without requiring distributed deployment.
Microservices
Microservices split the application into independently deployable services, each owning its data and communicating over the network through APIs or messages. Teams can deploy and scale a service independently when ownership and contracts support that autonomy.
The cost is operational complexity. A checkout may cross several services and data stores, requiring timeouts, tracing, contract compatibility, and recovery from partial failure. Adopt that cost when a concrete boundary needs independent ownership, deployment, scaling, or isolation and the team can operate it.
Quick reference
- Good fit when specific services require independent release, ownership, scaling, or failure isolation.
- Strengths: independent scaling, team autonomy, technology diversity, fault isolation.
- Weaknesses: network latency, distributed transactions, operational overhead, harder local dev.
- Start extraction with the most independent, highest-traffic module — not the core domain.
- Invest in observability (OpenTelemetry, centralized logging) before splitting services.
- Use an API Gateway as the single entry point for external clients.
Remember this
Microservices solve organizational scaling problems — adopt them when team boundaries demand it, not because it is trendy.
Recover One Partial Failure Across Services
Trigger. During checkout, Order Service calls Payment Service to charge the customer, and the call times out after 2 seconds — the network dropped the response, or Payment Service itself was slow. Symptom. Order Service doesn't know whether the charge actually succeeded on Payment Service's side before the timeout fired; treating the timeout as a definite failure and retrying blindly risks charging the customer twice, while treating it as a definite success without proof risks never charging them at all.
Root mechanism. A network timeout is fundamentally ambiguous — the request could have failed before reaching Payment Service, succeeded but the response was lost on the way back, or still be processing. This is exactly the boundary a monolith's in-process call never has to cross: an in-process function call doesn't leave this kind of ambiguity behind. Recovery: Order Service queries Payment Service for the charge's status using the same idempotency key it sent — Payment Service either confirms a charge exists for that key or confirms none does, resolving the ambiguity definitively. Verification: kill Payment Service mid-call in a test environment and confirm Order Service's retry logic queries status rather than blindly re-submitting the charge. Prevention: every cross-service call on a path with a side effect needs an idempotency key from the start — treating this as an edge case to add later is how double-charges make it to production.
Quick reference
- Trigger: a cross-service network call times out with no way to know if the far side completed.
- Symptom: blind retry risks a duplicate side effect; blind success assumption risks a missed one.
- Root mechanism: a network timeout is ambiguous in a way an in-process function call never is.
- Recovery: query the downstream service's actual state by the same idempotency key before deciding to retry.
- Prevention: idempotency keys belong on every cross-service call with a side effect from day one, not added after the first incident.
Remember this
A network timeout between services is genuinely ambiguous — recovery means querying the downstream service's actual state by idempotency key, never assuming either success or failure from silence.
Key takeaway
Choose from observable constraints, not headcount thresholds. A monolith or modular monolith remains valid while one release unit meets reliability and scaling needs; extract a service only when the boundary has an owner, data contract, failure plan, and measured reason for independent operation.
Practice (30 min): pick one module and record deploy frequency, change-failure rate, p95 resource use, incidents caused by the shared process, and cross-team wait time for four weeks. Write an extraction threshold before collecting data. If it is not met, keep the module in-process and add an architecture test that enforces its boundary.
Related Articles
Explore this topic