Modular Monoliths Win More Fights Than Microservices
TL;DR
Microservices can be the right answer, but they are often chosen for emotional reasons: status, scale theater, or a deep desire to debug network calls at 2 a.m. A modular monolith gives you most of the organizational benefits of separation without turning every local change into a distributed systems hobby. If your team is still small or your domain is still changing, that matters more than sounding grown-up.
The Seductive Lie of “Independent Services”
The microservices pitch is always charming:
- teams deploy independently
- scaling is surgical
- services stay focused
- the architecture is “future proof”
And in a perfect world, yes. In the same perfect world where every meeting ends early and every estimate is accurate.
In reality, microservices tend to introduce a new tax:
- network latency becomes business logic
- local debugging becomes archaeological work
- versioning becomes a coordination sport
- simple transactions become distributed sadness
Suddenly you are not just building software. You are operating a small internal internet.
What a Modular Monolith Actually Is
A modular monolith is one deployable unit with strong internal boundaries.
That means:
- one codebase
- one runtime
- one deployment pipeline
- many well-defined modules
Each module owns a slice of the domain. Modules talk through explicit interfaces, not by rifling through each other’s internals like raccoons in a pantry.
This is not “the big ball of mud, but with nicer comments.” The point is that boundaries exist in code and are enforced in review, imports, and tests.
Why This Usually Works Better
1. You Keep Transactions Simple
If two things must change together, keeping them in one process is not a moral failure. It is just practical.
Need to create an order and reserve inventory atomically? In a monolith, this is a transaction. In microservices, it becomes a saga, eventual consistency, retries, compensations, and a long Slack thread titled urgent-question-about-order-state.
2. Refactoring Stays Cheap
The best time to move a boundary is before you have paid for it in production incidents.
Inside a modular monolith, you can:
- rename a module
- move shared logic behind a clean interface
- split a package
- rewrite a hot path
without coordinating three deploys and a prayer.
3. Failure Modes Are Less Interesting
This is a compliment.
In a monolith, many failures are just failures. In microservices, failures are often ambiguous:
- was it auth?
- was it DNS?
- was it a timeout?
- was it the retry storm you accidentally built?
Less interesting failures are easier to fix. This is one of the few areas where boring is a feature.
How to Structure It
The easiest mistake is pretending folders are architecture. They are not. A folder named services does not stop anybody from importing everything from everywhere.
Use these rules instead:
- modules own their data and logic
- cross-module calls go through interfaces
- shared code stays genuinely shared, not just convenient
- dependencies point inward toward stable domain logic
Example:
src/
billing/
domain/
application/
infrastructure/
catalog/
domain/
application/
infrastructure/
shared/
The shared/ folder should stay small. If it becomes a landfill of “common” helpers, that is not architecture. That is denial.
When to Split
Sometimes the monolith does deserve to die a graceful death.
Split a module into a service when:
- it has a clearly independent lifecycle
- its scaling profile is very different
- multiple teams need to own it separately
- the operational boundary is worth the extra complexity
Do not split because:
- a consultant said “cloud-native”
- the org chart has a new director
- someone wants a diagram with more circles
- the team is bored
Boredom is not a scalability requirement.
The Real Tradeoff
The real question is not “monolith or microservices?”
The real question is:
Do you want complexity to live in code, or in the network, the deployment pipeline, and the people who have to wake up when it breaks?
Most teams are better served by local complexity and global simplicity. A modular monolith gives you that balance.
Final Rule of Thumb
If your team cannot confidently explain the domain boundaries inside a monolith, microservices will not fix that. They will just make the confusion harder to see and more expensive to ship.
Build the boundary first. Break it into services later if reality insists.
Continue reading
Next article
Software Architecture Is Mostly About Boundaries
Related Content
Software Architecture Is Mostly About Boundaries
A practical guide to drawing boundaries that survive contact with reality: APIs, modules, ownership, and the uncomfortable fact that most bugs are boundary bugs wearing a fake mustache.
Hexagonal Architecture with FastAPI: Database, Valkey Cache, Messaging
Code-heavy walkthrough of a document management platform built with Hexagonal Architecture in Python. Includes FastAPI adapters, SQLAlchemy persistence, Valkey caching, and message publishing.
REST API Design: Beyond the Dogma
A pragmatic look at REST API design for developers who've already made mistakes and want to stop making them.