DEV Community

Cover image for Modular Monolith or Microservices: A Framework for Deciding Without Dogma
Ivan Firestone
Ivan Firestone

Posted on

Modular Monolith or Microservices: A Framework for Deciding Without Dogma

Picture a team starting a new product. Before the first user signs up, the architecture diagram already has ten services, a message broker and an orchestrator. It looks modern. It looks scalable.

Three months later, a simple change, say adding a field to an invoice, touches four services, needs four coordinated deployments, and fails in a way nobody can trace. Every call that used to be a function call is now a network request, and the team spends more time keeping the pieces talking than building the product.

This is the distributed monolith: everything is still coupled, but now over the network. It's what happens when the architecture is chosen by fashion instead of by need.

After more than 20 years designing systems, my position is this: start with a modular monolith and extract services only when a concrete reason demands it. This article isn't a defense of the monolith. It's a framework for deciding based on criteria you can verify. The useful question was never "microservices or monolith?" but: what concrete problem am I solving, and how much does it cost to solve it this way?

Two Things That Get Confused

To answer that question, we first need to be clear about what we're comparing.

  • Modular monolith: a single deployment unit, but with modules that have clear boundaries, explicit public contracts and controlled dependencies.
  • Microservices: several independent deployment units, each with its own lifecycle and, ideally, its own database.

A modular monolith is not "the same old monolith". The difference is discipline: modules behave as if they were services, but they communicate within the same process.

So if the modular monolith already gives you boundaries, what exactly do you buy by going further?

The Bill Almost Nobody Reads

Moving to microservices is not free. What you gain in independence, you pay for in:

  • Network: latency, retries, timeouts and partial failures that didn't exist before.
  • Data consistency: ACID transactions across domains are gone. Sagas, compensations and eventual consistency come in.
  • Observability: without distributed tracing, correlated logs and per-service metrics, debugging an error becomes archaeology.
  • Deployment and operations: per-service pipelines, contract versioning, and multiplied configuration and secrets management.
  • Cognitive load: more pieces to understand, more boundaries to maintain and more places where a change breaks another team.

These costs are acceptable when you're buying something valuable in return. If not, it's debt with interest.

Which raises the fair question: when is the purchase worth it?

When Microservices Earn Their Place

There are real reasons to extract a service:

  1. Teams that need to deploy independently. If two teams block each other on every release, the service boundary solves a real organizational problem.
  2. Very different scaling profiles. One component that needs 50 replicas while the rest need 2.
  3. Failure isolation. A part that must not take down the rest, such as payment processing or bulk ingestion.
  4. Compliance or security requirements. Data that must live isolated, with its own access controls and auditing.
  5. Different technology out of real necessity, not preference: for example, a compute-intensive component that performs better in another language.

If your reason isn't on this list, you can probably wait. And while you wait, you'll want a design that keeps the door open.

Building a Monolith You Can Leave Later

The goal of a modular monolith is to make a future extraction boring. These are the rules that make it work:

  • One module per business context (Billing, Inventory, Users), aligned with domain boundaries, hand in hand with DDD.
  • An explicit public contract per module. Other modules only talk to that interface, never to the internals.
  • No cross-module table access. A module doesn't JOIN against another module's tables. Information is requested through the contract.
  • Internal events for decoupling. When something happens in a module, it publishes an event; the others react without knowing each other.
  • Rules verified automatically, not by goodwill.

That last rule is the one teams skip, and it's the one that matters most. Here's what it looks like in practice.

A Laravel Example

A simple structure:

app/
└── Modules/
    ├── Billing/
    │   ├── Contracts/        # the only thing other modules may use
    │   ├── Internal/         # private module details
    │   └── Events/
    └── Shipping/
        ├── Contracts/
        ├── Internal/
        └── Events/
Enter fullscreen mode Exit fullscreen mode

And an architecture test with Pest that enforces the boundary:

arch('Billing does not use Shipping internals')
    ->expect('App\Modules\Billing')
    ->not->toUse('App\Modules\Shipping\Internal');

arch('Shipping does not use Billing internals')
    ->expect('App\Modules\Shipping')
    ->not->toUse('App\Modules\Billing\Internal');
Enter fullscreen mode Exit fullscreen mode

If someone breaks the boundary, the CI pipeline fails. A boundary that isn't verified ends up being a suggestion. Tools like Deptrac serve the same purpose.

The Questions to Ask Before You Split

Now suppose a module starts to hurt. Before extracting a service, answer these questions:

Question If the answer is "no"
Is there a team that needs to deploy without coordinating with others? You probably don't need the service yet
Does this component have a clearly different load or scaling profile? Scaling the whole monolith is usually cheaper
Is there a failure-isolation or compliance requirement? A module with good boundaries may be enough
Do we have mature observability and deployment automation? Fix that first, or the operational cost will drown you
Is the domain boundary stable and well understood? Extracting the wrong boundary is expensive to revert

That last one is the most underestimated. A badly drawn boundary inside a monolith is fixed with a refactor. A badly drawn boundary between services is fixed with a migration.

How the Story Can Evolve

The good news is that this isn't a binary or permanent decision. A healthy path looks like this:

  1. Start with well-separated modules and automated rules.
  2. Measure: deployment times, blocking between teams, load hotspots.
  3. When a module meets one of the reasons on the list, extract it. Since it already has a public contract and doesn't share tables, the extraction is mechanical: the contract becomes an API or a set of events.
  4. Apply the strangler pattern: gradually route traffic to the new service without rewriting everything.

This way, the microservices decision is made with data and with the migration cost already reduced.

Where the Story Goes Wrong

Even with a good plan, some mistakes show up again and again:

  • A shared database between "microservices". It's a monolith with added latency.
  • Services that are too small (nanoservices). The coordination cost outweighs the benefit.
  • Extracting before understanding the domain. Boundaries are discovered over time, not on a whiteboard.
  • Ignoring the organization. Architecture ends up mirroring team structure; if they don't match, friction appears.

Back to That Team

Imagine the same team, starting over. This time they begin with a modular monolith, enforce the boundaries in CI, and measure. The invoice field is a one-module change. Months later, when one component needs to scale far beyond the rest, they extract it, and because the contract already exists, it takes days instead of a rewrite.

That's the whole argument:

  • A modular monolith is the reasonable default for most products.
  • Microservices are a tool with real costs; use it when you're buying something concrete in return.
  • Enforce boundaries automatically, so the option to extract services stays open.
  • Decide with measurable criteria, not with trends.

What criteria does your team use to decide when to extract a service? I'd love to hear your experience in the comments.

Top comments (0)