AJaiCodes logoAJaiCodes
HomeArticlesAbout
HomeArticlesThe Great Consolidation: From Micro-services back to "Modular Monoliths"

The Great Consolidation: From Micro-services back to "Modular Monoliths"

Ajanthan Sivalingarajah
·Mar 23, 2026·5 min read
The Great Consolidation: From Micro-services back to "Modular Monoliths"
JavaMicroservices ArchitectureSoftware ArchitectureMicroservicesSystem DesignDomain Driven DesignJPMSSpring BootSoftware EngineeringCloud Architecture
1 views
Software ArchitectureBackend EngineeringJavaSystem DesignCloud & DevOps
Vector API & Project Panama: High-Performance Java for AI and ML Workloads

AjaiCodes

A modern tech blog platform where developers share knowledge, insights, and experiences in software engineering and technology.

Quick Links

  • Home
  • Articles
  • About

Legal

  • Privacy Policy
  • Terms of Service

© 2026 AjaiCodes. All rights reserved.

The Great Consolidation: From Microservices back to "Modular Monoliths"#

Engineering High-Velocity Modular Monoliths with Java JPMS and Domain-Driven Design#


Part 1: The Case for Consolidation#

There was a time when breaking a monolith into microservices felt like unlocking a new level in engineering maturity. Teams moved faster, deployments became independent, and scaling suddenly felt elegant instead of painful.

Then reality arrived.

What looked like architectural sophistication started behaving like operational gravity. Latency crept in. Debugging turned into distributed archaeology. Observability became a full-time investment. Teams that once shipped features weekly found themselves tuning Kubernetes probes and chasing partial failures across services.

This isn't a rejection of microservices. It's a correction.

The Microservice Hangover#

Microservices introduce what many engineers now call the Distributed Systems Tax:

  • Network latency (milliseconds instead of nanoseconds)
  • Partial failures (timeouts, retries, circuit breakers)
  • Data consistency challenges (eventual consistency everywhere)
  • Operational overhead (Kubernetes, service meshes, tracing systems)
  • Cognitive load (understanding dozens of services)

For large organizations, this tax is acceptable---even necessary. For small to medium teams, it often becomes the bottleneck.

A simple mental model:

Operation TypeLatency
In-memory method call~10--100 ns
Same-process function~100--500 ns
Network call (same AZ)~0.5--2 ms
Cross-AZ network call~2--10 ms

That's a 1,000,000x difference in worst-case scenarios.

Now multiply that across service chains.

Defining the Modular Monolith#

A modular monolith is not a "big ball of mud."

It's a single deployable unit with strict internal boundaries:

  • Modules are isolated like services
  • Communication is explicit
  • Dependencies are controlled
  • Encapsulation is enforced at compile-time

The key distinction:

Legacy MonolithModular Monolith
Implicit dependenciesExplicit module graph
Shared mutable stateEncapsulated domains
Hard to refactorDesigned for extraction
No boundariesEnforced boundaries

The Architect's Perspective#

One of the most common architectural mistakes is conflating deployment concerns with code structure.

  • Microservices solve deployment independence
  • Modular monoliths solve codebase complexity

You don't need distributed systems to achieve modularity.

You need discipline---and enforcement.


Part 2: JPMS as the Enforcement Engine#

Java developers have long relied on packages for structure. But packages are advisory, not enforceable at scale.

The Problem with public#

In Java:

public class PaymentService { }

This is effectively visible to the entire codebase.

In a large system, that becomes dangerous:

  • Accidental coupling
  • Hidden dependencies
  • Tight integration between domains

Why Package-Private Isn't Enough#

Package-private (default) access:

  • Works only within the same package
  • Breaks down when modules grow
  • Encourages poor package design

You need something stronger.

Enter JPMS#

The Java Platform Module System (JPMS) introduces strong encapsulation at the module level.

Key Constructs

exports

Defines what is visible outside the module.

module com.app.orders {
    exports com.app.orders.api;
}
requires

Declares dependencies explicitly.

module com.app.orders {
    requires com.app.inventory;
}

Circular dependencies are caught at compile time.

opens

Used for reflection-heavy frameworks:

opens com.app.orders.internal to spring.core, spring.beans;
uses and provides

Enable service discovery via the Service Loader pattern:

uses PaymentProcessor;
provides PaymentProcessor with StripePaymentProcessor;

Part 3: Architecting Bounded Contexts#

Domain-Driven Design (DDD) fits naturally into modular monoliths.

Mapping Bounded Contexts to Modules#

Each bounded context becomes a JPMS module:

  • Orders → com.app.orders
  • Inventory → com.app.inventory
  • Shipping → com.app.shipping

Internal Structure of a Module#

com.app.orders
├── api
├── internal
└── infrastructure

api

package com.app.orders.api;

public interface OrderService {
    Order createOrder(CreateOrderCommand command);
}

internal

package com.app.orders.internal;

class OrderServiceImpl implements OrderService {
}

infrastructure

package com.app.orders.infrastructure;

@Repository
class OrderRepository {
}

Example: Module Definitions#

Orders Module

module com.app.orders {
    exports com.app.orders.api;

    requires com.app.inventory;
    requires spring.context;
    requires spring.beans;

    opens com.app.orders.internal to spring.core;
}

Inventory Module

module com.app.inventory {
    exports com.app.inventory.api;

    requires spring.context;
}

Part 4: Inter-Module Communication Patterns#

In-Process vs Network Calls#

Call TypeLatency
Method call~50 ns
REST call~5 ms

Synchronous Communication#

Order order = orderService.createOrder(cmd);

Asynchronous Communication#

public interface DomainEvent {}
public class OrderCreatedEvent implements DomainEvent {
    private final String orderId;
}
eventBus.publish(new OrderCreatedEvent(orderId));
@EventListener
public void handle(OrderCreatedEvent event) {
    inventoryService.reserve(event.getOrderId());
}
sequenceDiagram participant Orders participant EventBus participant Inventory Orders->>EventBus: Publish OrderCreatedEvent EventBus->>Inventory: Notify Inventory->>Inventory: Reserve Stock

Part 5: Data Sovereignty in a Monolith#

The Single Database Trap#

SELECT * FROM orders JOIN inventory ON ...

Logical Partitioning#

  • Schema per module
  • Table prefixing

Transactional Advantage#

@Transactional
public void placeOrder() {
    orderService.create();
    inventoryService.reserve();
}

Part 6: Practical Implementation & Tooling#

Gradle#

subprojects {
    apply plugin: 'java'
}

Observability#

Timer.builder("module.call")
    .tag("from", "orders")
    .tag("to", "inventory")
    .register(meterRegistry);
graph TD Orders --> Inventory Orders --> Payments

Comparative Analysis#


DimensionTraditional MonolithJPMS Modular MonolithMicroservices
DeploymentSingleSingleMultiple
LatencyLowVery LowHigh
ScalabilityLimitedModerateHigh
ComplexityLowModerateVery High
ConsistencyStrongStrongEventual
Team Size FitSmallSmall--MediumMedium--Large
Dev SpeedHighHighLow
Operational CostLowLow--ModerateHigh
DebuggingEasyModerateDifficult
Failure ModesLocalLocalDistributed

Part 7: The Break-Glass Strategy#

A well-structured modular monolith isn't a dead end.

It's a staging ground.

graph LR Orders --> InventoryModule InventoryModule --> InventoryService

Where This Is Heading#

The industry isn't abandoning microservices. It's becoming more selective.

Architecture is no longer about proving scalability.
It's about preserving velocity under real-world constraints.