SOFTWARE ARCHITECTURE / ARCHITECTURE PATTERNS
Modular Monoliths: The Underrated Default
A deliberate middle ground for teams that need speed and boundaries.
Key Insight: Most teams don't need microservices' independent deployability. They need microservices' clear boundaries. A modular monolith gives you the second thing without forcing you to pay for the first.
The architecture conversation too often gets framed as monolith versus microservices, as if those are the only two options and one of them is outdated. A modular monolith — a single deployable application, internally organized into well-bounded modules with enforced boundaries — is a legitimate, often better, default for a large share of systems, and it's underrepresented in how the industry talks about architecture choices.
What actually makes a monolith painful
The pain people associate with "monolith" — a codebase where every part of the system reaches into every other part, where a change in one area unpredictably breaks another, where nobody can safely work on it without deep whole-system knowledge — isn't caused by the deployment model. It's caused by the absence of internal boundaries. You can build a single deployable application with strict module boundaries and clear ownership, and you can build a distributed system of microservices with the exact same tangled coupling, just spread across network calls instead of function calls. Deployment topology and boundary discipline are separate axes, and conflating them is where a lot of "we need microservices" reasoning goes wrong.
What a modular monolith actually looks like
Each module owns its own data — logically, if not always physically separate schemas — and exposes a defined interface to other modules, the same way a service would expose an API. Modules communicate through those interfaces, not by reaching into each other's internal data structures directly. The boundary is enforced, ideally by tooling (module boundary linters, dependency-direction checks in CI), not just by convention that erodes the first time someone's under deadline pressure.
The result: one deployable, one process to run and monitor, one transaction boundary for operations that need it — and still the organizational clarity of knowing exactly what owns what, with the same discipline microservices are praised for.
What you get to skip
No distributed transactions or sagas for operations that touch multiple modules — a single database transaction handles it, correctly and simply, because it's still one process. No network latency or partial-failure handling between modules that are tightly related and usually change together. No service mesh, no per-service deployment pipeline, no distributed tracing infrastructure just to answer "what happened to this request." For a team of five to twenty engineers, this operational simplicity is a real, ongoing productivity advantage, not a consolation prize.
When it stops being the right answer
The honest limits: if different parts of the system genuinely need to scale independently at very different rates — one module handling ten times the load of another — a monolith forces you to scale the whole thing together. If different modules genuinely need independent deployment cadences because they're owned by teams that can't coordinate releases, that's a real pull toward service extraction. And if a module's boundary discipline keeps eroding despite enforcement — a real organizational signal, not just a technical one — that's sometimes a sign the team boundary and the module boundary should become the same boundary, which is what microservices give you.
The point isn't that microservices are wrong. It's that the decision should be made because those specific needs are real and present, not because modular monoliths are assumed to not scale.
Key takeaways
The pain associated with monoliths comes from missing internal boundaries, not from the deployment model itself. A modular monolith gets you enforced module boundaries, clear ownership, and independent-feeling development, while keeping the operational simplicity of a single deployable. Enforce module boundaries with tooling, not just convention. Extract a service when you have a real, present need for independent scaling or independent deployment cadence — not by default, and not because it's assumed to be the more mature choice.
