Skip to content

From Monolith to Modulith: The Pragmatic Middle Ground

From Monolith to Modulith: The Pragmatic Middle Ground
Michael Jauk
· Competitive Edge

“We need to migrate to microservices.”

We hear this regularly. Usually after a team has realized for the third time that a change in Module A broke something in Module C. The frustration is justified. The conclusion usually isn’t.

Microservices solve an organizational problem, not a code problem. If you’re struggling with an unstructured monolith, microservices won’t give you structured code - they’ll give you unstructured distributed code. With network latency, distributed transactions, and a Kubernetes cluster on top.

There’s a middle ground. One that works better in practice for most teams than either extreme.

What Is a Modulith?

A Modulith (Modular Monolith) is an application that deploys as a single unit - but is internally divided into clearly bounded modules. Each module encapsulates its own domain logic, services, and infrastructure. Communication between modules happens through defined interfaces, not direct access to internal classes.

The key difference from a classic monolith: in a Modulith, boundaries are enforced. Not through conventions in a wiki that get ignored after three weeks. But through tooling that fails the build when one module accesses another’s internals.

The difference from microservices: boundaries are enforced in-process, not over the network. This eliminates the entire complexity of distributed systems - service discovery, API gateways, distributed tracing, circuit breakers, eventual consistency.

MonolithModulithMicroservices
DeploymentSingle unitSingle unitIndependent per service
Code structureLayer-basedDomain-basedDomain-based
BoundariesNone or weakEnforced (build-time)Enforced (network)
CommunicationAnything goesPublic APIs/eventsHTTP/gRPC/messaging
Data ownershipShared everythingSchema per moduleDB per service
Ops complexityLowLowHigh
Refactoring costLowLowHigh

When a Modulith Is the Better Choice

There’s a simple rule of thumb by team size:

  • 1-10 developers: Monolith, but structured modularly from day one
  • 10-50 developers: Modulith. Structure without distribution complexity
  • 50+ developers: Selective microservice extraction - where it demonstrably pays off

In practice, the threshold is around 15-20 developers. Below that, the coordination overhead of microservices almost always exceeds the benefits.

More specific decision criteria:

Choose a Modulith when:

  • The domain is still evolving. Boundaries are easier to move within the same process than across service boundaries.
  • The team is under 30 developers and coordination is cheap.
  • No module has fundamentally different scaling requirements than the others.
  • The DevOps infrastructure isn’t set up for distributed systems yet.

Consider microservices when:

  • Multiple teams are blocking each other on deployments.
  • Individual modules need 10x more resources than the rest.
  • Compliance explicitly requires service-level isolation (PCI DSS, HIPAA).
  • The domain is stable and boundaries are proven.

Teams that build modularly first and then selectively extract report roughly 30% higher feature velocity compared to teams that start directly with microservices.

Migration in 5 Phases

Phase 1: Domain Discovery (Month 0-2)

Before touching any code: map domains. Together with product experts and developers. Which code belongs to which business capability? Where do things change frequently, where is it stable?

Domain-Driven Design isn’t an academic framework here - it’s a practical tool. Identifying Bounded Contexts means: finding where the natural fault lines in the system lie.

Phase 2: Modularization (Month 2-6)

This is the actual work:

  1. Restructure packages - Away from controller/service/repository, toward orders/shipping/billing. Domains instead of layers.
  2. Define public interfaces - Each module gets a facade. Direct access to internal classes is prohibited.
  3. Enforce boundaries with tooling - Packwerk (Ruby), ArchUnit (Java), dependency-cruiser (JS/TS). CI pipeline fails on violations.
  4. Push infrastructure to the module edge - Domain logic stays pure. Database access, external APIs, and messaging belong in each module’s infrastructure layer.
  5. Write module integration tests - Before anything gets extracted.

Phase 3: First Extraction (Month 6-8, only if needed)

Choose a small, well-bounded module. Auth or notifications are typical first candidates. Feature flags for rollout. Shadow traffic to compare old vs. new behavior.

Phase 4: Further Extractions (Month 9-12, only if justified)

Only where data, scaling, or team ownership clearly justify it.

Phase 5: Operations and Evaluation (from Month 13)

Most modules stay in the monolith. And that’s the right outcome. Decisions based on evidence, not ideology.

Data Isolation: The Underestimated Factor

One of the most common mistakes during modularization: all modules continue sharing the same database tables. This undermines every boundary.

Four levels of data isolation, from simplest to strongest:

Shared tables - All modules use one schema. Only convention separates access. Acceptable for the start, but fragile.

Schema per module (our recommendation) - Each module has its own schema in the same database. Modules may only access their own tables. Cross-module access only through the public API, never through direct queries.

Database per module - Highest isolation, but also highest operational overhead. Makes sense for compliance requirements or when extraction to a microservice is foreseeable.

Domain events - Modules publish events on state changes. Other modules maintain local read models. Eventual consistency - the path for long-term decoupled architectures.

The Most Common Mistakes

Big ball of mud with folders: Directory structure alone is not modularization. Without build-time checks, every boundary erodes within weeks.

Cross-module database writes: Module A writes directly to Module B’s tables. This destroys the entire premise. One writer per dataset. Always.

Premature extraction: Extracting modules as microservices before boundaries are stable. Result: a distributed monolith. The worst of both worlds.

Chatty cross-module calls: If a user action needs to call five or more modules synchronously, the boundaries are wrong.

No team ownership: Every module needs an explicit owner. Without ownership, nobody enforces boundaries.

When to Go Microservices After All?

Six reliable signals that a module is ready for extraction:

  1. Deployment bottleneck - Release coordination takes longer than feature development
  2. Divergent scaling - One module needs 10x the resources of the others
  3. Independent delivery cycles - Teams block each other on releases
  4. Incident blast radius - Failures in one module regularly take down other modules
  5. Compliance isolation - Regulatory requirements demand service-level separation
  6. Technology mismatch - A module needs a different runtime or language

If none of these signals apply: stay with the Modulith. That’s not a compromise - it’s the right architecture decision.

Conclusion

Microservices are a tool, not a religion. The Modulith isn’t a stepping stone on the way to microservices - it’s the target architecture for the majority of teams.

The pragmatic path: understand domains. Enforce boundaries. Define modules. And only extract where data, scaling, or team autonomy demonstrably demand it.

Shopify runs 2.8 million lines of Ruby as a Modulith and handles 284 million requests per minute on Black Friday. The architecture scales. The question is whether your problem is really an architecture problem - or a structural problem in existing code.

The Self-Test

Which architecture fits your team? Five questions, one recommendation.

Architecture Check

Monolith, Modulith or Microservices? Find out in 5 questions.

Question 1 / 5

How large is your development team?

Share this post

Related Service

Digital Consulting

Every project starts with a conversation.

Let us talk about your individual needs and goals.

Start a project