Microservices are often introduced as an answer to a team problem: independent delivery, clearer ownership, and the ability to scale parts of a system separately.
Those are real benefits. They are also expensive benefits. A distributed system adds network failure, deployment coordination, observability requirements, data-consistency decisions, and a larger operational surface area before the product has necessarily earned any of them.
For many teams, a modular monolith is a better starting point: one deployable application with explicit internal boundaries. It keeps local calls and a single operational unit while making the architecture honest about ownership and dependencies.
The problem is not one process
A monolith becomes difficult when its boundaries are implicit:
- ›any package can reach into any other package;
- ›domain rules are scattered across controllers, services, and repositories;
- ›database tables become the accidental integration contract;
- ›a small change requires understanding the whole application; and
- ›teams organize around technical layers rather than business capabilities.
The deployment unit is not the root problem. The problem is uncontrolled coupling.
Splitting that application into services may hide some coupling behind HTTP or messaging, but it does not remove the underlying design problem. It can also make every design mistake more expensive to diagnose.
Start with business modules
A modular Spring Boot application should be organized around capabilities rather than global technical layers:
com.example.store
├── StoreApplication.java
├── orders
│ ├── OrderManagement.java
│ ├── OrderController.java
│ ├── OrderRepository.java
│ └── internal
├── inventory
│ ├── InventoryApi.java
│ ├── InventoryController.java
│ └── internal
└── customers
├── CustomerApi.java
└── internal
The names are not the important part. The important part is that each module has an intentional public surface and keeps implementation details private.
An order module might expose a use case such as placeOrder(). It should not expose its JPA entities, repositories, or internal services for other modules to call directly. Inventory can answer a meaningful question—such as whether stock can be reserved—without allowing every caller to manipulate its persistence model.
Make boundaries executable
Package structure is a useful convention, but conventions decay. A modular monolith needs tests or architecture checks that fail when a boundary is crossed accidentally.
Spring Modulith provides one option for Spring Boot applications. Its default model treats direct subpackages of the application package as application modules. It can verify module relationships, detect cycles, and identify access to another module's internals.
A verification test can be small:
class ModularityTests {
@Test
void modulesShouldRespectTheirBoundaries() {
ApplicationModules.of(StoreApplication.class).verify();
}
}
The specific tool is less important than the invariant: an architectural boundary should be checked by automation rather than remembered by every developer.
Prefer use cases over shared internals
The most common failure mode is a shared “common” package that becomes a back door around every module. A second is allowing one module to import another module's persistence classes because it is convenient.
Prefer a small API at the boundary:
public interface Inventory {
Reservation reserve(ProductId productId, Quantity quantity);
}
The implementation can change from a local method call to an event or remote service later. The rest of the application depends on a business capability, not on the current storage or transport mechanism.
That does not mean every method needs an interface. Abstraction has a cost too. Introduce a boundary where there is a meaningful capability, ownership decision, or likely change—not merely because a class exists.
Keep transactions honest
One advantage of a monolith is that a transaction can often cover a business operation without crossing a network boundary. That advantage should be used carefully.
Transactions should still be owned by a module and express a business operation. Avoid a service method that updates several unrelated modules' tables simply because they happen to share a database. If an operation crosses a real business boundary, consider a domain event, an explicit workflow, or a compensating action—even while the code runs in one process.
This makes a later extraction possible without pretending that extraction is free.
Testing becomes more useful
Modularity supports a useful testing pyramid:
- ›module-level tests verify business rules close to the code;
- ›boundary tests verify allowed dependencies and published APIs;
- ›a smaller number of application tests verify wiring and critical flows; and
- ›end-to-end tests verify the deployed system rather than every internal detail.
The goal is not to eliminate integration tests. It is to stop every test from requiring the entire application and a shared set of fragile fixtures.
When a modular monolith is not enough
Choose separate services when there is a concrete reason, such as:
- ›independently scaling workloads with materially different resource profiles;
- ›genuinely separate availability or security boundaries;
- ›independent deployment ownership that the organization can operate;
- ›a technology/runtime boundary that is worth the cost; or
- ›a clear need to isolate failure domains.
“We may need to scale someday” is not, by itself, a sufficient reason. Neither is the desire to look modern on an architecture diagram.
A practical migration path
A modular monolith does not trap a team in one deployment model. It creates a better extraction seam:
- ›Identify a capability with a stable boundary.
- ›Stop new code from reaching into its internals.
- ›Replace direct internal calls with a module API or domain event.
- ›Measure the operational and organizational reason for extraction.
- ›Move the module only when the benefits exceed the distributed-systems cost.
The important decision is not “monolith or microservices.” It is whether the system has boundaries that make either choice manageable.
The decision
For a new Spring Boot system, I would start with a modular monolith unless there is a specific reason not to. Organize by business capability, publish narrow APIs, verify boundaries automatically, and keep transactions and data ownership explicit.
That gives a team room to learn from a working system before committing to a fleet of distributed ones.