Microservices

Modular Monolith vs Microservices: How to Choose

Modular monolith vs microservices: start with a modular monolith and extract services only when a real force demands it. The decision framework and the signals.

Part of Microservice Service Design: Boundaries That Hold
Modular monolith vs microservices, shown as a segmented solid cube beside a distributed node mesh split by an amber divider

Modular monolith vs microservices has a clear default answer for almost every new system: start with a modular monolith, and extract microservices only when a specific force demands it. The modular monolith gives you most of the discipline of good service boundaries, strong module separation, clear ownership, without paying the network latency, operational complexity, and distributed-systems failure modes that microservices impose from day one.

The industry over-rotated on microservices and many teams adopted them before they had the problems microservices solve. The result was distributed monoliths: all the cost of services, none of the independence. The corrective is not “monoliths good, microservices bad” but “earn your way into microservices.”

Why this is the most consequential architecture choice

The monolith-versus-microservices decision shapes your deploy model, your team structure, your failure modes, and your operational cost for years. It is also the decision most often made for the wrong reasons, resume-driven development, hype, or a belief that microservices are simply more “scalable,” rather than a real, present need.

Getting it right early saves enormous pain, because reversing it is expensive in both directions: splitting a tangled monolith is hard, and re-merging premature microservices is harder. This post is part of the Service design series.

What is the difference between a modular monolith and microservices?

A modular monolith is a single deployable application with strong internal boundaries between modules; microservices take those same modules and run them as independently deployable services that communicate over the network. Both demand real boundaries. The difference is what crosses them: in a monolith, an in-process function call; in microservices, a network call with all the latency, partial failure, and serialization that implies.

Modular monolithMicroservices
Deploy unitOne applicationMany independent services
Module callsIn-process (fast, reliable)Network (latency, partial failure)
BoundariesEnforced in codeEnforced by the network
Refactoring boundariesCheap (move code)Expensive (re-split services)
Independent scalingNo (scale the whole app)Yes (scale per service)
Operational complexityLow (one thing to run)High (mesh, tracing, deploys)
Team autonomyShared deployIndependent deploys

The key insight is that the modular monolith and microservices share the most important property, real module boundaries. A well-built modular monolith is not spaghetti; it is microservices’ boundary discipline without the network tax. That is what makes it the right starting point and a clean springboard to extract services later.

Should you start with a monolith or microservices?

Start with a modular monolith for almost every new system. Early on you do not yet know where the right service boundaries are, because you do not yet fully understand the domain, and a monolith lets you move boundaries with a refactor instead of a cross-service migration. Locking in service boundaries before you understand the domain is how you end up with the wrong services and a distributed monolith.

The modular monolith also keeps the door open. Because its modules already have clean boundaries, extracting one into a service later is a contained operation, not a rewrite. You get the option to go distributed without paying for it before you need it.

When should you move from a modular monolith to microservices?

Extract a microservice when a concrete force appears that the monolith cannot satisfy, and extract only that service for that specific reason. The legitimate forces are independent scaling, team autonomy, fault or security isolation, and a genuinely different technology need for one component. “It would be more modern” is not on the list.

The real triggers for extraction:

  • Independent scaling. One component has a wildly different load profile and needs to scale on its own.
  • Team autonomy. A team needs to deploy on its own cadence without coordinating with the rest of the app.
  • Fault isolation. A risky or heavy workload should not be able to take down the whole application.
  • Security isolation. A component handles sensitive data or untrusted input and needs a separate boundary.
  • Different technology. One piece genuinely needs a different language or runtime, like a Rust hot path.

When one of these appears, extract that single module into a service, because that module has that need. Do not use it as a reason to dissolve the whole monolith. The strangler-style, one-service-at-a-time extraction is how mature systems migrate, and it keeps you in control the whole way.

Is a modular monolith better than microservices?

For most teams at most stages, starting with a modular monolith is the better call. It delivers the boundary discipline that makes microservices valuable, while avoiding the network latency, operational overhead, and distributed-systems failure modes that microservices add. Microservices are better, specifically, when you have the forces above, which is a narrower set of situations than the last decade of hype suggested.

The honest tradeoff is independence versus simplicity. Microservices buy independent deployment, scaling, and isolation, and they pay for it with cross-service boundaries that break in subtle ways, distributed tracing, and a much heavier operational footprint. A modular monolith keeps things simple and gives up that independence until you actually need it. Choose based on which set of problems you have right now, not which architecture sounds more advanced.

Does a modular monolith scale?

Yes, much further than most teams assume. A single well-built application on modern hardware, scaled horizontally behind a load balancer, handles very large traffic, and the database is almost always the real scaling limit long before the application tier is. “We need microservices to scale” is usually a misdiagnosis; what most systems actually need first is a faster data layer, caching, and more instances of the same monolith.

The distinction that matters is between scaling throughput and scaling independently. A monolith scales throughput fine by running more copies of itself, which is simple and effective. What a monolith cannot do is scale one component independently of the rest, so if a single subsystem needs ten times the resources of everything else, running ten times as many full monoliths to feed it is wasteful. That specific inefficiency, not raw capacity, is a legitimate trigger to extract that subsystem.

So the honest answer is that scaling is rarely the reason to leave a monolith; independent scaling of an imbalanced component sometimes is. Diagnose which one you actually have before splitting. Many teams adopt microservices for “scale” and discover their bottleneck was the database all along, which microservices did nothing to fix and in fact made harder to reason about.

A modular-monolith-vs-microservices checklist

Use this when deciding for a new system or component:

  • Default to a modular monolith with real, enforced module boundaries.
  • Confirm you do not yet have a concrete force requiring independent services.
  • If you do, name the specific force (scaling, autonomy, isolation, technology) for the specific component.
  • Extract that one component as a service, not the whole system.
  • Keep module boundaries clean in the monolith so future extraction stays cheap.
  • Re-evaluate as the domain and team grow; the right answer changes over time.
  • Avoid splitting on hype, resume-building, or a vague sense that services are “more scalable.”

What does a modular monolith require to stay modular?

The usual objection to the modular monolith is that modules erode into a big ball of mud. That happens, and it happens because nothing enforced the boundaries — modularity that depends on discipline alone reliably loses to deadlines.

What actually holds:

Compile-time or build-time enforcement. Modules should be unable to import each other’s internals, enforced by the build rather than by review. Language-level module systems, build-tool visibility rules, or an architecture test that fails CI on a forbidden import all work. Without one, the first urgent fix reaches across a boundary and the precedent is set permanently.

Separate schemas per module. A shared database schema is what makes extraction impossible later. Give each module its own tables and forbid cross-module joins, so the data boundary exists even inside one process. This single practice does more than any other to preserve the option of splitting later.

Explicit interfaces between modules. Each module exposes a deliberate API and hides everything else. In-process calls, but through a defined surface — which is what lets you replace an in-process call with a network call later without touching callers.

No shared mutable state. No global caches, no shared session objects, no reaching into another module’s data structures. State crossing module boundaries silently is the coupling that is hardest to find when you eventually try to extract.

The reason these matter beyond tidiness: they are precisely the properties that make later extraction possible. A modular monolith with enforced boundaries and separate schemas can have a module lifted into a service in weeks. One where modules share tables and reach into each other is a rewrite, and teams in that position usually conclude that microservices are hard when what is actually hard is the mess they are extracting from.

That is the real argument for starting with a modular monolith: it is not a lesser architecture, it is the same architecture with the network removed — and you can add the network later, per module, when a specific module has earned it.

What does each architecture actually cost to operate?

The design debate usually stops at the code and skips the operational bill, which is where most of the real difference lives.

DimensionModular monolithMicroservices
Local developmentRun the appRun several services, or mock them
DebuggingOne stack trace, one debuggerDistributed tracing, correlated logs
A single changeOne deployPossibly several, in an order
TransactionsDatabase transactions workSagas, compensation, eventual consistency
TestingIn-process integration testsContract tests plus a staging environment that resembles production
InfrastructureOne deployable, one pipelinePer-service pipelines, service discovery, mesh or gateway
On-callOne system to understandOwnership per service; incidents span teams
ScalingScale the whole appScale the hot service only

Only the last row favours microservices, and it is genuinely valuable when one component’s resource profile differs sharply from the rest. Everything else is a cost, and those costs are paid continuously by every engineer, every day, whether or not you are currently getting value from the split.

The row worth dwelling on is transactions. In a monolith, “update these three things atomically” is a database transaction. Across services it becomes a saga with compensating actions, partial-failure states, and reconciliation — a genuinely hard distributed-systems problem introduced by an architectural choice rather than by the domain. If your workflows are transactional by nature, this cost alone can dominate the comparison.

The honest summary: microservices buy independent deployability and independent scaling, and charge for it in operational complexity across every other dimension. That trade is excellent when you have many teams needing to ship without coordinating, or a component whose scaling profile is genuinely different. It is a poor trade for a small team on a system that would fit comfortably in one deployable — which is most systems, most of the time.

How do you extract a service from a modular monolith?

When a module has genuinely earned its own service, the extraction is a sequence rather than a rewrite — and doing it in the wrong order is what makes people believe extraction is hard.

  1. Enforce the module boundary first. No cross-module imports, no cross-module joins. If this step is difficult, the extraction was never going to work, and you have discovered it before spending anything.
  2. Give the module its own schema. Still the same database instance, still the same process — but its own tables, with all other access going through the module’s interface. This is usually the longest step and it is where the real coupling is found.
  3. Route all calls through an explicit interface. Everything else in the monolith talks to the module through one API surface. Still in-process.
  4. Make the interface async where it will need to be. If the eventual service will be called over a network, calls that are currently synchronous may need to become asynchronous. Discovering that now, in-process, is vastly cheaper than discovering it after the network is between you.
  5. Deploy the module as a separate process, same codebase. Same code, different deployable, calls now crossing a network. This is the step that surfaces timeouts, retries, and serialisation issues, and it is reversible.
  6. Split the repository and the database only once it has run in production this way.

The important property: each step is independently valuable and independently reversible. Steps 1 to 3 improve the monolith whether or not you ever extract, which means the work is not wasted if priorities change. Most failed extractions attempt step 6 first and then discover the coupling from steps 1 and 2 with a network already in the middle.

The corollary for teams still deciding: the preparation for extraction and good monolith hygiene are the same work. That is what makes “modular monolith first” a low-risk default rather than a bet — you are not deferring the architecture, you are building the part of it that survives either decision.

What is the honest decision rule?

Stripped of preference, the choice comes down to a small number of conditions. Microservices are the right call when several are true at once:

  • Multiple teams need to ship independently and are currently blocked by each other. This is the original and strongest justification — the architecture solves an organisational problem before a technical one.
  • A component’s scaling profile genuinely differs. One part needs ten times the resources of the rest, or different hardware entirely.
  • A component needs a different runtime. Latency-critical work, or a library that exists in one language.
  • Fault isolation is required. One component’s failure must not take the rest down, and process isolation is the only way to guarantee it.
  • You can operate them. Tracing, per-service ownership, deployment automation, and on-call rotations already exist or will.

One condition alone is usually not enough. A single component with unusual scaling needs can often be extracted on its own while everything else stays together — which is the hybrid most mature systems actually run, and it is rarely the thing anyone sets out to build.

The conditions that do not justify it, and are the ones most often cited: the team wants to try it, the architecture diagram looks more professional, a conference talk described it, or the codebase feels large. Codebase size alone is a modularity problem, and splitting a poorly-factored monolith into services produces poorly-factored services with a network between them — which is strictly worse, because you have added latency and partial failure to a structure that was already hard to change.

The default for a small team is therefore a modular monolith with enforced boundaries, extracting individual services when a specific one earns it. That is not a compromise position; it is the arrangement that keeps the most options open at the lowest ongoing cost, and it is what the extraction sequence above is designed to support.

One last framing that helps when the debate stalls: ask what you would have to undo if you are wrong. Choosing a modular monolith and discovering you need services means extracting modules — real work, incremental, reversible at each step. Choosing microservices and discovering you did not need them means merging services, consolidating databases, and unwinding a deployment topology across teams, which is substantially harder and almost never actually attempted. When two options have asymmetric regret, the cheaper mistake is the better default.

That asymmetry is also why the industry’s loudest lesson of the last decade — teams publicly consolidating services back into fewer deployables — is less a reversal than a correction. The systems that split early paid for a topology their organisation did not need, and the ones that split late, per component, on evidence, mostly did not have to write that blog post.

What I’d do differently

The mistake the whole industry made, and I have made, is reaching for microservices as the default for greenfield systems because they signaled scale and sophistication. The cost was distributed monoliths: teams paying the full operational price of services while still being unable to deploy independently, because the boundaries were wrong.

If I were starting a new system today, I would build a modular monolith with boundaries as disciplined as if they were services, and I would extract services only when a real, named force appeared, one service at a time. That path gives you simplicity now, the option of distribution later, and the best chance of getting boundaries right, because you draw them after you understand the domain rather than before. Microservices are a tool for specific problems, not a maturity level you graduate to.

Sources

Frequently asked questions

What is the difference between a modular monolith and microservices?

A modular monolith is a single deployable with strong internal module boundaries; microservices split those modules into independently deployable services over a network. Both enforce boundaries, but the monolith keeps one deploy and in-process calls, while microservices add independent deploys at the cost of network calls and operational complexity.

Should you start with a monolith or microservices?

Start with a modular monolith for almost every new system. You rarely know the right service boundaries early, and a monolith lets you move them cheaply. Extract microservices later, when a specific force (independent scaling, team autonomy, isolation) actually demands it.

When should you move from a modular monolith to microservices?

When a concrete force appears: a component needs to scale independently, a team needs to deploy without coordinating, a workload needs fault or security isolation, or a part needs a different technology. Extract that specific service for that specific reason, not the whole system at once.

Is a modular monolith better than microservices?

For most teams at most stages, yes to start. A modular monolith delivers much of the boundary discipline of microservices without the network and operational tax. Microservices win when you genuinely need independent deployment, scaling, or isolation, which is a smaller set of cases than the hype suggests.

How do you stop a modular monolith degrading into a big ball of mud?

Enforce boundaries in the build rather than in review: forbid cross-module imports of internals, give each module its own database schema and forbid cross-module joins, expose explicit interfaces, and allow no shared mutable state. These are also exactly the properties that make later extraction to services possible.

What do microservices cost compared with a modular monolith?

More on nearly every operational dimension: local development, debugging, deploy coordination, testing, infrastructure, and on-call. Only independent scaling favours them. Transactions are the sharpest cost, since a database transaction becomes a saga with compensating actions and partial-failure states.

How do you extract a service from a modular monolith?

In order: enforce the module boundary, give it its own schema, route all calls through an explicit interface, make calls async where the network will require it, deploy as a separate process with the same codebase, and split the repository last. Each step is reversible and improves the monolith regardless.

When are microservices genuinely the right choice?

When several conditions hold at once: multiple teams blocked by each other, a component whose scaling profile genuinely differs, a component needing a different runtime, a hard fault-isolation requirement, and the operational capability to run them. Codebase size alone is a modularity problem, not a microservices one.