Back to Articles
📄
microservicesmodular monolithdistributed systemsoperational complexityservice decompositiondomain-driven designCI/CDeventual consistency

Microservices vs. Modular Monoliths: When to Split and When to Stay

Microservices promised endless scalability, but hidden costs are driving teams back to monoliths. A well‑designed modular monolith can keep the benefits while slashing operational overhead and latency headaches.

Microservices vs. Modular Monoliths: When to Split and When to Stay

The microservices paradigm promised independent deployability, team autonomy, and horizontal scalability. Many engineering organizations adopted distributed services between 2015 and 2020. By 2026, many of those organizations are consolidating distributed services to eliminate network serialization overhead, distributed tracing complexity, and cascading failures. A well-architected modular monolith delivers modular domain boundaries and fast compilation without distributed systems operational tax. Consolidation initiatives typically follow production outages caused by network partitions, retry storms, and uncoordinated schema migrations.

What Went Wrong with Microservices

Microservices are not inherently bad.
They are genuinely the right choice for certain problems.
But the pattern was widely adopted without the organizational and technical prerequisites that make it work.

The costs that were underestimated:

Distributed systems complexity. Every service boundary introduces network calls, serialization, partial failure, and eventual consistency. A monolith that crashes is a single failure. A distributed system that partially fails is a debugging nightmare. Martin Fowler's "microservices premium", the additional complexity cost of distribution, is only worth paying when the benefits are real and measurable. This is because the complexity introduced by distributed systems can often outweigh the benefits, making debugging and maintenance significantly more challenging. When a request traverses several services, a failure in any one of them propagates as latency spikes or incomplete responses, forcing engineers to trace across logs, network traces, and multiple codebases.

Operational overhead. Each distributed microservice demands a dedicated CI/CD pipeline, container registry lifecycle, liveness/readiness probes, distributed tracing propagation, and alerting thresholds. A fleet of 50 microservices multiplies the operational failure surface area fifty-fold. Without mature internal developer platforms, engineering capacity shifts from business domain delivery to distributed infrastructure maintenance.

Premature decomposition. Services were split along technical lines (a "user service," a "notification service") rather than domain lines. When a feature required changes to three services, the supposed independence evaporated, it just became a distributed monolith with all the coordination costs and none of the benefits. Each cross‑service transaction introduced latency and required compensating actions, eroding the promised speed of independent development. This premature decomposition can lead to unnecessary complexity, making it harder to manage and maintain the system.

Data consistency. Distributed transactions are hard. Sagas and eventual consistency are correct solutions, but they require careful design and add significant complexity to every cross‑service operation. Many teams discovered this after splitting their database. When a write spans two services, coordinating commit or rollback demands additional messaging and state tracking, which often surfaces as subtle bugs in production. This complexity can lead to data inconsistencies and reliability issues, making it difficult to ensure that the system operates as intended.

The Modular Monolith

A modular monolith is a single deployable unit organized into well‑defined, loosely coupled modules.
Each module owns its domain logic and data.
Modules communicate through explicit interfaces, not shared database tables or direct object references.

The key insight is that the benefits of microservices, team autonomy, independent development, clear domain boundaries, come primarily from modularity, not from distribution.
A well‑modularized monolith can provide most of those benefits without the operational overhead of a distributed system.
This approach allows teams to achieve the benefits of microservices without the added complexity and operational burden.
Because the code runs in a single process, latency is minimal and testing remains straightforward.

Modular Monolith Structure:

src/
  modules/
    orders/
      OrderService.ts       ← public interface
      OrderRepository.ts    ← owns its data access
      OrderEvents.ts        ← domain events
      internal/             ← private implementation
    inventory/
      InventoryService.ts
      InventoryRepository.ts
      internal/
    notifications/
      NotificationService.ts
      internal/
  shared/
    events/                 ← cross-module event bus
    types/                  ← shared value types only

The rule: modules communicate only through their public interfaces. No module reaches into another module's internal/ directory. No module queries another module's database table directly. This strict adherence to clear and well‑defined interfaces helps maintain the integrity and modularity of the system, making it easier to manage and scale over time. When a module respects this contract, internal refactoring never ripples outward, preserving stability.

Spring Modulith

Spring Modulith is a framework for building modular monoliths in Java.
It enforces module boundaries at the package level and provides tools for verifying that modules do not violate their declared interfaces.

// Spring Modulith: module boundary enforcement
@ApplicationModule(allowedDependencies = "shared")
package com.example.orders;

// This will fail the architecture test if orders/ imports from inventory/internal/

Spring Modulith also supports publishing domain events between modules through Spring's application event mechanism, which can later be replaced with a real message broker if the module needs to be extracted into a service.
This makes the transition to a microservices architecture smoother and more manageable, should the need arise.

Domain-Driven Design as the Guide

The right boundaries for modules (and, if needed, services) come from the domain, not from technical layers.
DDD's bounded contexts provide the conceptual tool: a bounded context is a boundary inside which a particular domain model is valid and consistent.
A bounded context maps naturally to a module.
The Orders context owns the concept of an order.
The Inventory context owns the concept of stock.
They communicate through domain events, not through shared objects.
By aligning modules with business capabilities, teams can own a complete slice of functionality without stepping on each other's code.

// Wrong: shared domain object crosses module boundary
import { Order } from '../orders/internal/Order'; // ❌ reaches into internal

// Right: communicate through public interface and events
import { OrderPlacedEvent } from '../orders/OrderEvents'; // ✅ public contract

class InventoryModule {
  @OnEvent('order.placed')
  async handleOrderPlaced(event: OrderPlacedEvent): Promise<void> {
    await this.reserveStock(event.orderId, event.items);
  }
}

When Microservices Are the Right Answer

Microservices are justified when specific conditions are met, not as a default architecture.

Independent scaling requirements. If one component needs to scale to 100x the load of the rest of the system, and that scaling is genuinely independent, a separate service makes sense.
A video transcoding pipeline that needs 500 workers while the rest of the application needs 5 is a real case.
This independence allows for more efficient use of resources and better performance.

Independent deployment requirements. If different parts of the system need to be deployed on different schedules by different teams with different risk tolerances, service boundaries enforce that independence.
This requires mature CI/CD and platform engineering capabilities to ensure that the deployment processes are well‑managed and efficient.

Technology heterogeneity. If one component genuinely needs a different runtime, language, or infrastructure (a Python ML inference service alongside a TypeScript API), a service boundary is the natural solution.
This allows for flexibility and the use of the most appropriate technology for each component.

Organizational scale. Conway's Law states that systems mirror the communication structures of the organizations that build them.
If you have 20 teams that cannot coordinate effectively, a modular monolith will become a coordination bottleneck.
Microservices can give each team genuine autonomy, but only if the team is large enough to own the operational overhead.
This can lead to more efficient collaboration and better alignment with organizational structures.

In production systems evaluated by architecture review, teams only adopt microservices architectures after evaluating horizontal scaling limits and domain decomposition across independent bounded contexts.

The Migration Path

The modular monolith is also a useful intermediate step for teams that want to eventually extract services.
A well‑modularized monolith makes extraction straightforward: the module boundary is already defined, the interface is already explicit, and the data ownership is already clear.

Step 1: Modular monolith with in-process events
  Orders module → [in-process event bus] → Inventory module

Step 2: Replace event bus with durable broker (no interface change)
  Orders module → [Kafka topic] → Inventory module

Step 3: Extract Inventory to a separate service (no domain change)
  Orders service → [Kafka topic] → Inventory service

The reverse is also valid.
Several high‑profile teams, including Shopify, Stack Overflow, and Amazon Prime Video, have publicly described consolidating microservices back into monoliths or modular monoliths when the operational overhead outweighed the benefits.
This flexibility allows teams to adapt their architecture to better meet their needs as they evolve over time.
It lets you keep a unified deployment pipeline while still experimenting with asynchronous communication.

The Honest Decision Framework

Question Microservices Modular monolith
Team size Multiple large teams One team or a few small teams
Deployment frequency Services deploy independently Coordinated releases are acceptable
Scaling requirements Components scale independently Uniform scaling is sufficient
Operational maturity Strong platform engineering Limited platform investment
Domain clarity Boundaries are well understood Domain is still being discovered
Current state Greenfield or mature domain Early-stage or evolving domain

The most common mistake is choosing microservices before the domain is well understood.
Splitting a system along the wrong boundaries is expensive to undo.
A modular monolith lets you discover the right boundaries first, then extract services when the boundaries are proven and the operational investment is justified.
Early splitting forces you to guess at ownership, which often clashes with real usage patterns discovered later.
This approach can save a lot of time and effort in the long run, as it allows for a more flexible and manageable architecture.

Why Wrong Boundaries Are So Costly to Fix

When a service boundary is drawn in the wrong place, the cost is not just refactoring.
Every team that has built against that boundary has written code that assumes it.
Every API contract, every event schema, every data ownership decision is built on top of the wrong decomposition.
Undoing it means coordinating changes across multiple teams, multiple repositories, and multiple deployment pipelines simultaneously, exactly the kind of coordination that microservices were supposed to eliminate.
This can be a significant challenge, especially in large, complex systems.

This is why the modular monolith is not just a simpler architecture, it is a risk management strategy.
Module boundaries in a monolith are cheap to move.
You refactor the code, run the tests, and deploy once.
Service boundaries in a distributed system are expensive to move.
You negotiate API changes, coordinate deployments, manage data migrations, and update every consumer.
The cost of getting the boundary wrong is an order of magnitude higher in a distributed system than in a monolith.
This makes modular monoliths a safer and more cost‑effective option, especially when the domain is still evolving.

Physical service boundaries should be established only after domain boundaries have stabilized through production traffic and clear bounded contexts. Carving boundaries prematurely locks organizations into distributed data partitioning and multi-service joins before access patterns are understood.

Sources & References

  1. [1]
  2. [2]
  3. [3]
  4. [4]
  5. [5]
  6. [6]
  7. [7]
  8. [8]
  9. [9]
  10. [10]

Comments (0)

Loading comments...
Microservices vs. Modular Monoliths: When to Split and When to Stay | Arne's Blog