Back to blog
Modular monolith architecture compared to microservices
System Design

Modular Monolith: Why Teams Are Leaving Microservices

May 21, 2026 10 min read Avinash Tyagi
modular monolith microservices software architecture system design monolith vs microservices modular monolith vs microservices when to use microservices microservice architecture monolithic applications ArchUnit

I spent two years helping teams decompose monolithic applications into microservices. I configured service meshes, set up distributed tracing, built circuit breakers, and debugged cascading failures at 2 AM. Then I watched three of those teams quietly merge their services back into a single deployable.

They weren't admitting defeat. They were making the smartest architectural decision of their careers. If you have ever debated modular monolith vs microservices with your team, this post will give you the data and framework to settle that conversation.

The Microservices Hangover Is Real

Something shifted in the industry around 2024. The CNCF's own survey data showed that 42% of organizations that adopted microservice architecture were consolidating services back into larger deployable units. Not because the technology failed. Because the economics stopped making sense.

Here is what nobody warned those teams about when they started splitting services:

The infrastructure costs multiplied. Each service needed its own CI/CD pipeline, its own monitoring, its own alerting, its own on-call rotation. A 10-person team running 30 microservices was spending more time on operational plumbing than on building features.

The debugging got brutal. A single user request touching seven services meant correlating logs across seven different systems. Distributed tracing helped, but it added yet another piece of infrastructure to maintain.

The data consistency headaches never stopped. Sagas, outbox patterns, eventual consistency. All valid patterns. All significantly harder to reason about than a database transaction.

Amazon Prime Video's team told this story most clearly when they migrated their Video Quality Analysis system from distributed microservices to a monolith and cut infrastructure costs by 90%. Not a typo. Ninety percent.

What a Modular Monolith Actually Is

A modular monolith is not your grandmother's big ball of mud. It is a single deployable application with strict internal module boundaries, where each module owns its data, exposes a clean API to other modules, and could theoretically be extracted into a separate service if needed. Unlike monolithic applications of the past, a modular monolith applies the same separation of concerns that microservice architecture promotes, but without the network overhead.

The key word is "boundaries." Without them, you just have a monolith. With them, you get most of the development velocity benefits of microservices without the operational overhead.

Think of it this way: in a microservice architecture, services independently deploy and communicate over the network. A modular monolith enforces boundaries through code architecture and build tooling instead. The boundaries exist in both cases. The enforcement mechanism is different. The question of when to use microservices versus a modular monolith comes down to whether you need those network-level boundaries or whether code-level boundaries are sufficient.

Modular monolith with strict module boundaries vs microservices with network boundaries
Modular monolith vs microservices: same boundaries, different enforcement

The Three Rules of Module Boundaries

A well-structured modular monolith follows three rules:

1. Each module owns its database schema. Module A does not read from Module B's tables directly. If Module A needs data from Module B, it calls Module B's public API (an in-process function call, not an HTTP request).

2. Module dependencies are explicit and acyclic. If Module A depends on Module B, that dependency is declared in the build system. Circular dependencies are caught at compile time, not discovered during a production incident.

3. Public APIs are thin. Each module exposes a narrow interface. Internal implementation details stay internal. You should be able to rewrite a module's internals without touching any other module.

Here is what this looks like in practice with a Java (Spring Boot) project:

OrderService.javajava
// Module: orders/api/OrderService.java (PUBLIC interface)
public interface OrderService {
    OrderDTO createOrder(CreateOrderRequest request);
    OrderDTO getOrder(UUID orderId);
    List<OrderDTO> getOrdersByCustomer(UUID customerId);
}

// Module: orders/internal/OrderServiceImpl.java (PRIVATE implementation)
@Service
class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepo;
    private final InventoryService inventoryService;

    @Override
    @Transactional
    public OrderDTO createOrder(CreateOrderRequest request) {
        inventoryService.reserveStock(request.getItems());
        Order order = Order.create(request);
        orderRepo.save(order);
        return OrderDTO.from(order);
    }
}

Notice what is happening here. The OrderService interface is the public contract. The implementation class is package-private (no public modifier). Other modules can only interact through the interface. The InventoryService dependency is explicit and injected, not a hidden database query.

Enforcing Boundaries with ArchUnit

Rules only work if they are enforced. In a modular monolith, you use tools like ArchUnit (Java), Boundary (TypeScript), or custom build rules to make illegal dependencies fail the build:

ArchitectureTests.javajava
@ArchTest
static final ArchRule modules_must_not_access_other_modules_internals =
    noClasses()
        .that().resideInAPackage("..orders.internal..")
        .should().accessClassesThat()
        .resideInAPackage("..inventory.internal..");

@ArchTest
static final ArchRule no_circular_dependencies =
    slices().matching("com.levelop(..)..")
        .should().beFreeOfCycles();

When to Use Microservices (They Are Not Dead)

I'm not arguing that microservices are dead. That take is as lazy as the one claiming every app needs them. Understanding when to use microservices versus a modular monolith requires looking at your specific constraints. Microservice architecture remains the right choice in specific scenarios:

  • Independent scaling requirements. If one part of your system handles 100x more traffic than the rest, you need services that independently deploy and scale.
  • Different technology stacks. If your ML team needs Python and your backend runs Go, separate services make sense.
  • Organizational independence. If you have genuinely separate teams with different business units and compliance requirements, microservice architecture reduces coordination costs.
  • Regulatory isolation. Payment processing under PCI-DSS, health data under HIPAA. Sometimes you need hard process boundaries enforced by infrastructure.

The honest assessment from Thoughtworks is that organizations with fewer than 50 engineers rarely see net benefits from microservices. The modular monolith vs microservices debate should always start with your team size and operational capacity.

The Practical Migration Path: Majestic Monolith First

The architectural pattern winning most often in 2026 is what DHH calls the "Majestic Monolith with extracted hot paths." One well-structured codebase, with two or three genuinely-separated services for the parts that have a real reason to live alone.

Step 1: Identify Your Hot Path Services

Map your service dependency graph. Find the services that genuinely need independent scaling or technology isolation. In most systems, this is 2-3 services out of 20-30.

Step 2: Merge Everything Else

Take the remaining services and merge them into a single application with module boundaries. This sounds scary but is usually straightforward because the service boundaries already tell you where the module boundaries should be.

migration-result.txttext
Before: 25 microservices, 25 CI/CD pipelines, 25 monitoring dashboards
After:  1 modular monolith + 3 extracted services = 4 deployables

Step 3: Add Architecture Tests Immediately

Before the first PR is merged, add ArchUnit/Boundary tests that enforce module isolation. Without these, the merged codebase will become a big ball of mud within months.

Step 4: Consolidate Infrastructure

You just went from 25 deployables to 4. Delete 21 CI/CD pipelines. Remove 21 monitoring configurations. Reduce your Kubernetes manifests by 80%. This is where the cost savings become very real.

Real-World Performance: The Numbers

The cost reduction is not just theoretical. Published case studies show consistent patterns:

Shopify runs one of the largest e-commerce platforms in the world on a modular monolith (Ruby on Rails). They handle millions of transactions per day from a single deployable application with strict module boundaries.

Amazon Prime Video's team reported that moving from microservices to a monolith reduced their infrastructure costs by 90% and improved their scaling capability.

A 2026 survey by O'Reilly found that teams running modular monoliths reported 40% faster feature delivery compared to equivalent teams running microservices. The primary reason: less time spent on cross-service coordination and infrastructure maintenance.

How to Structure Your Modular Monolith from Scratch

If you are starting a new project, here is the folder structure that works:

project-structuretext
src/
  modules/
    orders/
      api/           # Public interfaces and DTOs
      internal/      # Private implementation
      persistence/   # Repository and entity classes
      events/        # Domain events
    inventory/
      api/
      internal/
      persistence/
      events/
    customers/
      api/
      internal/
      persistence/
      events/
  shared/              # Truly shared code
  infrastructure/      # Database config, messaging

Each module's api/ folder contains only interfaces and DTOs. The internal/ folder contains the implementation. Build rules prevent any module from importing another module's internal/ package.

Inter-module communication happens through direct method calls via injected interfaces (synchronous) or domain events via an in-process event bus (asynchronous). Both approaches keep modules decoupled.

OrderCreatedEventHandler.javajava
@Component
public class OrderCreatedEventHandler {
    private final NotificationService notificationService;

    @EventListener
    public void handle(OrderCreatedEvent event) {
        notificationService.sendOrderConfirmation(
            event.getCustomerId(),
            event.getOrderId()
        );
    }
}

The Decision Checklist

Before you choose your architecture, run through this checklist:

  • Your team has fewer than 50 engineers? Start with a modular monolith.
  • All your services share the same language and runtime? Modular monolith.
  • You are spending more than 30% of engineering time on infrastructure? Consider consolidating.
  • You have genuine independent scaling needs? Extract those as services. Keep everything else in the monolith.
  • You need different technology stacks? Microservices for those components. Modular monolith for the rest.

The answer is almost never "all microservices" or "one big monolith." It is usually "a well-structured modular monolith with a few extracted services." If you want a deeper look at the tradeoffs, read our guide on system design fundamentals or explore more architecture patterns on Levelop.

Frequently asked questions

What is the difference between a modular monolith and a traditional monolith?

A traditional monolith has no internal boundaries. Any code can call any other code, access any database table, and create hidden dependencies. A modular monolith has strict module boundaries enforced by build tools and architecture tests. Each module owns its data, exposes a clean API, and cannot access another module's internals.

Can you migrate from microservices back to a modular monolith?

Yes, and many teams are doing exactly this in 2026. The migration path involves identifying which services genuinely need independence (usually 2-3), then merging the rest into a single application with module boundaries.

How do you handle database migrations in a modular monolith?

Each module owns its own database schema. Migrations are organized per-module. Most teams use a single database instance with schema-level isolation rather than separate databases.

Is a modular monolith slower than microservices?

For most workloads, a modular monolith is faster because inter-module communication happens through in-process function calls instead of network requests. A function call takes nanoseconds. An HTTP request takes milliseconds.

How do you prevent a modular monolith from becoming a big ball of mud?

Architecture tests are the answer. Tools like ArchUnit (Java) or Boundary (TypeScript) catch illegal cross-module dependencies at build time. Run these tests in CI so no PR can merge if it violates module boundaries.

Keep reading

System Design

3 System Design Patterns Every Engineer Should Know

Master three essential system design patterns — Layered Architecture, Pub/Sub Messaging, and CQRS — with practical examples and guidance on when to use each.

Read article
System Design

Vanishing Links: I Designed a URL Shortener and the Expiry Logic Was the Hard Part

A URL shortener is a lifecycle problem, not a mapping one. Full system design with expiry logic, three services, and the data layer that ties it all together.

Read article