The Modular Monolith Revival: Why Teams Are Consolidating After a Decade of Microservices
After ten years of splitting everything into services, the industry is quietly merging things back. This is not a reversal — it is a correction. What the modular monolith actually is, why distribution was the wrong default, how to decide which model fits your team, and how to consolidate without a rewrite.
Around 2015, "we're moving to microservices" was the default answer to almost any architectural question. Netflix and Amazon had proven the model at scale, Docker made deployment tractable, and Kubernetes arrived to orchestrate it. For most of a decade, splitting things up was simply what good engineering looked like.
The mood has shifted. Amazon Prime Video published a piece about consolidating a distributed service back into a monolith and cutting costs by 90%. DHH's writing on the same theme hit a nerve big enough to become a genre. "Modular monolith" went from apologetic to respectable. Talk to teams building new systems in 2026 and a lot of them are starting with one deployable unit — on purpose, not out of naivety.
I want to be precise about what happened, because the hot-take version ("microservices were a mistake") is wrong and unhelpful. Microservices solve real problems. The industry just adopted them as a default rather than as a solution to problems most teams didn't have, and the correction is now underway.
What Microservices Actually Cost
The pitch was independent deployability, independent scaling, team autonomy, and fault isolation. Those benefits are real. What the pitch left out is that a network boundary converts every in-process concern into a distributed systems problem:
| In a monolith | Across services |
|---|---|
| Method call | Network call — can be slow, can fail, can succeed-then-fail |
| Compiler catches signature changes | Contract breakage found at runtime, in production |
| Database transaction | Saga, compensating actions, eventual consistency |
| Stack trace | Distributed trace across N services, if you built one |
| Refactor across modules in one PR | Coordinated deploys across repos and teams |
| Run the app locally | Run 14 services locally, or develop against a shared env |
None of these are insurmountable. All of them cost engineering time that isn't going into your product. For a fifteen-person team, that overhead can consume a startling share of total capacity — I've watched teams spend more effort on service scaffolding, contract versioning, and local-dev tooling than on the features those services existed to deliver.
The costliest item isn't on that table, though: getting the boundaries wrong. A badly-placed module boundary inside a monolith is a refactor. A badly-placed service boundary is a migration — coordinated across teams, repos, databases, and deploy pipelines. And boundaries are hardest to get right exactly when teams choose microservices: at the beginning, before the domain is understood.
What a Modular Monolith Actually Is
The term gets used loosely, so: a modular monolith is a single deployable unit with strictly enforced internal boundaries. One process, one deploy, one codebase — but modules that cannot reach into each other's internals.
The critical word is enforced. Every legacy big ball of mud started as a codebase with folders and good intentions. What makes a modular monolith different is that the boundaries are mechanically defended:
- Module-private internals. Java's module system (JPMS), Spring Modulith, .NET internal visibility, Go's
internalpackages, or explicit ArchUnit rules in the build. The build fails when a module imports another's internals. - Explicit public APIs. Each module exposes a deliberately small surface — an interface, a facade — and everything else is invisible.
- No shared tables across modules. This is the one people skip and the one that matters most. Each module owns its tables; other modules go through its API. Same database, separate schemas or a table-ownership convention enforced in review. If two modules write the same table, you don't have modules.
- Events for cross-module reactions. In-process domain events, so module A doesn't call module B synchronously for side-effectful workflows. This is also what makes a later extraction feasible.
Done properly, the module map reads like a service map — the difference is that calls are in-process, transactions are real transactions, and a boundary change is a refactor rather than a migration.
The Actual Decision
The useful question is never "monolith or microservices" in the abstract. It's which specific forces apply to you. My honest decision criteria:
Choose microservices when:
- Team count, not code size, demands it. The genuine driver is independent deployment by independent teams. Around 4–5 autonomous teams, coordination on a single deploy pipeline starts to hurt. Below that, you're paying distribution costs to solve a coordination problem you don't have.
- A component has a genuinely divergent scaling profile. Video transcoding, ML inference, or a scraping fleet that needs 50 instances while the rest of the app needs 3. Extracting that one thing is almost always correct — and note it's an extraction, not a wholesale split.
- Fault isolation is a hard requirement. A component whose failure must not take down the rest, for regulatory or contractual reasons.
- A component has genuinely different technology needs. A Python ML service next to a Java platform.
- Compliance requires physical isolation of certain data or processing.
Choose a modular monolith when:
- You have fewer than about four teams.
- The domain is still being discovered — which is true of nearly every product before it finds real usage.
- Your traffic scales roughly uniformly across features.
- Your operational maturity is modest. Microservices need service discovery, distributed tracing, contract testing, and on-call depth. Without them, you get all the costs and none of the benefits.
- You're optimizing for iteration speed, which — early on — is the only thing that matters.
The strongest version of this advice: start with a modular monolith, take the boundaries seriously, and extract services when a specific module presents a specific reason — scaling, isolation, or team ownership. Extraction from a well-modularized monolith is genuinely straightforward. Retrofitting boundaries onto a big ball of mud, or merging back a premature split, is not.
Why the Shift Is Happening Now
Four forces converged, and it's worth naming them because they tell you whether the correction applies to your situation.
Cost discipline returned. In the zero-interest-rate era, "spin up another service and another cluster" didn't require justification. It does now. Distribution has a direct infrastructure bill — inter-service traffic, per-service redundancy, the observability stack needed to see across it all — and finance teams started asking about it.
Hardware got absurd. A lot of microservice splitting was implicitly about scaling limits. A single modern server with hundreds of gigabytes of RAM and dozens of cores handles a workload that would have required a fleet in 2015. Most companies' entire production load fits comfortably on hardware that costs less than the engineers maintaining the split.
The tooling for in-process modularity matured. Spring Modulith, JPMS, and the architecture-test ecosystem (ArchUnit and friends) made boundaries enforceable without a network hop. In 2015, the network was the only reliable enforcement mechanism — which is a real part of why teams reached for it.
Ten years of evidence accumulated. Enough teams have now lived through a premature split, the eighteen-month migration, and the discovery that their distributed monolith was worse than the monolith — services that must deploy together, share a database, and fail together. That's the anti-pattern the whole correction is reacting against, and it's depressingly common.
How to Consolidate Without a Rewrite
If you've already over-split, the fix is incremental, not a big-bang rewrite. What I'd do:
- Find the services that always deploy together. Check your deploy history. Two services that ship in lockstep aren't independent — they're one service in two repos, paying the network tax for nothing. These are your merge candidates, and they're usually obvious once you look.
- Find the services that share a database. Same conclusion, stronger evidence. Shared persistence means no independent evolution.
- Merge them as modules, not as a blob. When you consolidate service A and B, they become module A and module B inside one deployable — keeping their public APIs, losing the HTTP layer between them. The network call becomes a method call; the contract stays.
- Keep the genuinely independent services independent. This is a correction, not a reversal. The ML inference service with its own scaling profile stays out.
- Measure before and after. Deploy frequency, lead time, incident rate, infra cost, and local-dev setup time. If consolidation doesn't move those, you consolidated the wrong things.
The Real Lesson
The modular monolith revival isn't an argument that distribution is bad. It's an argument that modularity and distribution are different things, and the industry conflated them for a decade.
You need modularity always — clear boundaries, owned data, explicit contracts, low coupling. That's just good design, and a monolith without it becomes unmaintainable regardless of how many services you eventually split it into.
Distribution is one implementation of modularity, and an expensive one. It buys independent deployment, independent scaling, and fault isolation, and it charges you network failure, eventual consistency, operational complexity, and rigid boundaries. Buy it when you need what it sells.
The teams that got this right in 2015 weren't the ones who split everything. They were the ones who modularized ruthlessly and distributed selectively. That was always the right answer — it just took a decade of collective experience for it to become the popular one.
Frequently asked questions
What is a modular monolith?
A single deployable unit with strictly enforced internal module boundaries: module-private internals, explicit public APIs, no shared tables across modules, and in-process domain events for cross-module reactions. The enforcement is mechanical — via the module system, architecture tests, or visibility rules — which is what distinguishes it from a conventional monolith that merely has folders.
Are microservices dead?
No. Microservices solve genuine problems: independent deployment across many autonomous teams, divergent scaling profiles, hard fault isolation, and compliance-driven separation. What is ending is their use as a default architecture for teams that have none of those problems. The correction is toward distributing selectively rather than reflexively.
When should I split a modular monolith into microservices?
When a specific module presents a specific reason: it needs to scale independently of the rest, its failure must be isolated, a separate team needs ownership and an independent deploy cadence, or it needs different technology. Extract that module. Team count is usually the strongest signal — around four to five autonomous teams, coordinating on one deploy pipeline starts to cost more than distribution does.
What is a distributed monolith and why is it bad?
A set of services that must be deployed together, share a database, or fail together. It pays every cost of distribution — network failures, eventual consistency, operational complexity, coordinated releases — while delivering none of the benefits, since nothing is actually independent. It is usually worse than the monolith it replaced, and it is the specific outcome the modular monolith revival is reacting against.
How do I merge microservices back together without a rewrite?
Incrementally. Identify services that always deploy together or share a database — those are already one service in multiple repos. Merge them as separate modules inside one deployable, keeping their public APIs and dropping only the HTTP layer between them, so a network call becomes a method call. Leave genuinely independent services alone, and measure deploy frequency, lead time, incident rate, and infrastructure cost before and after.
Go deeper: the System Design roadmap covers microservices architecture, domain-driven design, and hexagonal and clean architecture, while the Backend Engineer track works through monolith vs microservices and inter-service communication from a practical Spring Boot angle.
More from Microservices
Browse more articles and guides on this topic.