
The Modular Monolith Is Not a Compromise, It Is Usually the Correct Answer
A modular monolith gives you enforced boundaries, independent modules and a single transaction. Microservices give you the same boundaries plus a network, and the network is the part that costs you.
The modular monolith gets described as a stepping stone, something you tolerate until the team is ready for real architecture. That framing is backwards. A modular monolith and a microservices system make the same claim about the code, which is that modules have owners, explicit interfaces and no reaching into each other's internals. The difference is that one of them puts a network between the modules. Everything people find hard about microservices comes from that network, and almost nothing they find valuable does.
Start with what the split does not give you. It does not give you module boundaries. If your monolith is a ball of mud where any class can query any table, extracting services does not fix that, it distributes it. You end up with the same tangle, now with serialisation, retries and partial failure between the tangled parts. Boundaries are a discipline applied to code, and if you can enforce them in a distributed system you could have enforced them in one process.
What the network actually buys
Three things, and it is worth being precise because these are the only legitimate reasons to pay for it.
Independent deployment. Modules ship without coordinating a release. This matters when release coordination is the binding constraint, which happens at a specific organisational size and not before. With three teams on a fortnightly cadence it is not the constraint. With thirty teams shipping several times a day it is the whole problem.
Independent scaling. One component needs different hardware or a different scale factor. Video transcoding, ML inference, a crawler. This is real, and it is also the case that most systems have one or two such components rather than fifteen, which argues for extracting those two and leaving the rest alone.
Fault isolation. A failure in one module cannot take down the others. Genuine, but weaker than it sounds, because a synchronous dependency on a failing service takes you down anyway unless you have built and tested fallbacks. Isolation comes from the fallback, not from the process boundary.
Notice what is absent from that list. Code quality, clean interfaces, team autonomy over a codebase, testability, and the ability to reason about a subsystem in isolation are all available in a single deployable, for free, if you enforce them. The comparison is laid out in more detail in the guide on choosing between a modular monolith and microservices.
What the network costs
| Concern | Modular monolith | Microservices |
|---|---|---|
| Cross-module write | One database transaction | Saga, outbox, compensation, reconciliation |
| Cross-module read | Function call, in-process | Network call with timeout, retry, circuit breaker |
| Refactoring a boundary | Rename and move, one commit | Versioned contract, two deploys, migration window |
| Debugging a request | One stack trace | Distributed tracing you must build and maintain |
| Integration testing | Run the app | Test containers, contract tests, or hope |
| Local development | Start one process | Start N, or mock them and lose fidelity |
| Failure modes | Exception | Timeout, partial write, duplicate delivery, out-of-order events |
The row that quietly does the most damage is refactoring a boundary. Early in a system's life you get boundaries wrong, and you should expect to. In one process, moving a responsibility from module A to module B is an afternoon. Across services it is a contract change, a data migration, a coordinated deploy, and a period during which both shapes must be supported. Distributed architecture makes your boundary mistakes expensive at exactly the point in a product's life when you are most likely to be making them, which is a good part of the argument in the guide on when microservices are the wrong choice.
The enforcement problem, which is real
The honest objection to the modular monolith is that boundaries in one codebase decay. Nothing stops a developer importing an internal class at four in the afternoon under deadline pressure. A network boundary is enforced by physics; a module boundary is enforced by intent.
That is true and it is solvable with mechanism rather than discipline. The tools differ by language but the pattern does not:
- Split the codebase into modules or packages with declared dependencies, so a disallowed import fails compilation rather than review.
- Add an architecture test to CI that asserts the dependency graph. Rules like "billing must not import fulfilment internals" become a failing build, not a comment on a pull request.
- Give each module its own schema or table prefix and forbid cross-schema queries. This is the important one, because data coupling is what actually blocks a future split.
- Route all cross-module interaction through a published interface in the module's public package. Everything else is internal by default.
- Assign an owner per module in the code owners file so changes get reviewed by people who care about the boundary.
Do that and you have the discipline of services without the distribution. You have also done every piece of preparation a future extraction needs, because a module with its own schema and a published interface is a service that has not been deployed separately yet.
When to actually split
Split when one of the three benefits above is a live problem you can describe in a sentence, not a projected one. Release coordination is delaying shipping. A specific component needs different hardware or an independent scale curve. A specific failure has repeatedly taken down unrelated functionality and no in-process fallback fixes it.
Split narrowly when you do. Extract the one component with the scaling requirement and leave the rest in the monolith. A system with a monolith and three services around it is a normal, healthy shape, not a transitional state to apologise for. If and when the write patterns and the traffic justify more extractions, the staged approach to migrating out of a monolith applies, and it works far better from a modular starting point than from a mud ball.
What to do next
Write down the specific problem the split is meant to solve, in one sentence, with a name attached to it. If the sentence is about code quality, module ownership, or testability, the answer is enforcement in the current codebase and you can start this week. If it is about release coordination, scaling a named component, or an isolation failure that has actually happened, you have a case, and it probably justifies extracting one or two things rather than decomposing everything.
Then add the architecture test to CI regardless of which way that goes. It is the cheapest work available, it improves the system you have today, and it is the prerequisite for any split you might do later. If the answer turns out to be a genuine extraction, our microservices architecture practice is generally arguing for the smallest split that solves the stated problem.
Fastnexa Engineering
Software Development Team at Fastnexa. We write from real client work, and we are happy to talk through yours.
Ready to ship this?
Bring this problem to a free 30-minute call with the team that wrote the post.
Book a demoMore from the blog
View all
Observability Is a Prerequisite for Splitting a Monolith, Not a Follow-Up Task
Almost every migration schedules tracing after the first services ship. That ordering removes the stack trace before anything replaces it, and it hides the data you needed to choose the boundaries.

The Take-Home Test Filters for Free Time, Not Engineering Ability
Take-home exercises select the candidates with the most spare evenings and the least verification, and the alternative costs an hour of an engineer's time rather than eight of a candidate's.

Service Boundaries Drawn on an Org Chart Will Not Survive Contact With the Data
Team names make convenient service names and terrible service boundaries. The real seams are visible in your write patterns and your transaction logs, not in the reporting structure.
Related services
Want help putting this into practice? Here is how we deliver it.