For a decade, "we're moving to microservices" was the default answer to any architecture question. Many teams paid for it with distributed transactions, a dozen deployment pipelines, network latency between services that used to be method calls, and debugging sessions across five log streams. The industry has since corrected course: a well-structured modular monolith gives most of the benefits of clear boundaries without the operational cost. This guide explains when each architecture fits, and how to build a modular monolith in Spring Boot with Spring Modulith that can still be split into services later.

The real trade-offs

Microservices offer independent deployment, independent scaling, technology freedom per service and fault isolation. They cost you distributed system complexity: network failures, eventual consistency, data duplication, versioned APIs, distributed tracing and much more infrastructure. Martin Fowler summarized the trade-offs years ago, and his advice still holds: almost all successful microservice stories started with a monolith that grew too big and was broken up (Fowler: MonolithFirst; Microservice trade-offs).

Real-world examples point the same way. Shopify chose to modularize its large Rails monolith rather than split it into services (Shopify Engineering). Amazon's Prime Video team moved an audio/video monitoring service from a distributed serverless design to a single process and reported a 90% infrastructure cost reduction for that workload (Prime Video Tech, 2023).

Factor Modular monolith Microservices
Team size One to a few teams Many autonomous teams
Deployment One artifact Many, independent
Data consistency Local transactions Eventual consistency, sagas
Refactoring boundaries Cheap (same codebase) Expensive (APIs, data migration)
Operational overhead Low High
Independent scaling Coarse Fine-grained
Failure isolation Process-level Service-level

The rule of thumb we use: start with a modular monolith unless you already have several teams that need to deploy independently, or parts of the system with radically different scaling or availability needs.

What makes a monolith "modular"

A modular monolith is one deployable application divided into modules with explicit boundaries:

  • Each module owns a business capability — orders, inventory, billing, notifications — and its data.
  • Modules expose a small public API; everything else is internal.
  • Modules communicate through that API or through events, never by reaching into each other's internals or tables.
  • Dependencies between modules are deliberate and acyclic.

Without enforcement, these rules erode within months. That is where Spring Modulith helps.

Spring Modulith in a nutshell

Spring Modulith is a Spring project for building modular monoliths with Spring Boot. Version 2.0, released in November 2025, is based on Spring Boot 4 and Spring Framework 7 and reworked the event publication registry (Spring blog, 2025; Spring Modulith reference).

Its core convention is simple: each direct sub-package of your main application package is an application module, and only types in the module's root package are its public API; sub-packages are internal (Spring Modulith: fundamentals).

com.acme.shop
├── ShopApplication.java
├── order            ← module "order": public API in this package
│   ├── OrderService.java
│   ├── OrderPlaced.java          (event)
│   └── internal                  ← not accessible to other modules
│       ├── OrderRepository.java
│       └── PricingCalculator.java
├── inventory
│   ├── InventoryService.java
│   └── internal/...
└── notification
    └── internal/...

Verify boundaries in a test

A single test fails the build when a module uses another module's internals or when dependencies form a cycle (Spring Modulith: verification):

class ModularityTests {
    private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

    @Test
    void verifiesModularStructure() {
        modules.verify();
    }

    @Test
    void writesDocumentation() {
        new Documenter(modules).writeDocumentation();   // C4 and PlantUML diagrams of modules
    }
}

The documentation generator produces diagrams of modules and their dependencies straight from code, so architecture docs stay current (Spring Modulith: documentation).

Communicate through events

Direct calls between modules are fine for queries. For reactions — "when an order is placed, reserve stock and send a confirmation" — use application events so the order module doesn't depend on inventory and notification (Spring Modulith: events):

// order module
@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher events;
    private final OrderRepository orders;

    @Transactional
    public Order place(PlaceOrder command) {
        Order order = orders.save(Order.from(command));
        events.publishEvent(new OrderPlaced(order.getId(), order.getLines()));
        return order;
    }
}

// inventory module
@Component
class InventoryListener {
    @ApplicationModuleListener          // async, transactional, after the publisher commits
    void on(OrderPlaced event) {
        inventory.reserve(event.orderId(), event.lines());
    }
}

The event publication registry stores events in the database within the publishing transaction and marks them completed when listeners succeed, so events are not lost if the application crashes — the transactional outbox pattern without extra infrastructure. Events can also be externalized to Kafka, AMQP and other brokers when another system or a future microservice needs them.

Test modules in isolation

@ApplicationModuleTest bootstraps only one module (and optionally its dependencies), and the Scenario API lets you assert that a module publishes or reacts to events (Spring Modulith: testing). Tests stay fast even as the application grows.

From modular monolith to microservices, if you need it

Because modules already have explicit APIs, own their data and talk through events, extracting one into a service is a contained project:

  1. Pick the module with a clear reason to be separate: different scaling, a separate team, different release cadence.
  2. Externalize its events to a broker; other modules keep consuming them.
  3. Replace direct calls to its API with an HTTP or messaging client behind the same interface.
  4. Move its tables to a separate database.
  5. Deploy it independently; observe with distributed tracing, see OpenTelemetry observability.

Most modules never need this step, and that is the point: you pay the distribution cost only where it buys something.

Practical tips

  • Model modules on business capabilities, not technical layers. "controllers", "services" and "repositories" are not modules.
  • One schema per module in the same database, or at least table prefixes, makes data ownership visible.
  • Keep the public API small. If another module needs internals, the boundary is probably wrong.
  • Combine with virtual threads for high-throughput I/O in a single process; see Virtual threads in Spring Boot.
  • Works with Kotlin too; see Spring Boot + Kotlin in 2026.

FAQ

Is a modular monolith just a monolith with folders? No. The difference is enforced boundaries: module APIs, verified dependencies and event-based communication, checked in tests on every build.

Can a modular monolith scale? Yes, horizontally like any stateless service. You scale the whole application rather than individual modules, which is fine for most workloads.

Do we need Spring Modulith, or can we use ArchUnit? ArchUnit can enforce rules too. Spring Modulith adds Spring-aware module detection, event registry, module tests and documentation on top.

When should we definitely choose microservices? When several teams must deploy independently at high frequency, or when parts of the system have very different scaling, availability or regulatory requirements.

Sources

  1. Martin Fowler. MonolithFirst and Microservice trade-offs.
  2. Shopify Engineering. Deconstructing the monolith.
  3. Prime Video Tech (2023). Scaling up the Prime Video audio/video monitoring service and reducing costs by 90%.
  4. Spring (2025). Spring Modulith 2.0 GA released.
  5. Spring Modulith reference: fundamentals, verification, events, testing, documentation.