Spring Modulith is a submodule inside the Spring framework that helps developers structure their monolithic Spring applications in a more organized and maintainable way. It addresses the common challenge of code becoming complex and intertwined as applications grow larger.
At its core, Spring Modulith introduces the concept of application modules—distinct parts of your application that handle specific business capabilities. These modules maintain clear boundaries and communicate through well-defined interfaces.
<span class="pink">The challenge before was that developers could easily break module boundaries and create unwanted dependencies, as there was no enforcement.The code would still work, but the modular structure would gradually break down.</span>
Previously, maintaining strict module boundaries in a Spring Boot application was challenging. Developers could inadvertently create unwanted dependencies, breaking the modular structure over time. While the code would still function, the architecture would lose its integrity.
Spring Modulith introduces a solution to enforce module boundaries effectively. By integrating this dependency, the system actively prevents such mistakes. If you attempt to access restricted code or establish improper dependencies between modules, your tests will fail. This proactive approach ensures your modular structure remains clean, consistent, and maintainable as your application evolves.
For instance, in an e-commerce system, you might have separate modules for order processing, inventory management, and customer accounts.
The framework works with standard Spring Boot applications and uses conventional package structures to define module boundaries.
To use Spring Modulith effectively, your project should be running on:
💡 In Spring Modulith, each top-level package within the root package is treated as a separate module. Any public class, interface, or other public element directly within these module packages is considered a "public API," making it accessible to other modules so that modules can communicate with each other using these public APIs.
However, if a public element is located within a subpackage of a module (nested package), Spring Modulith treats it as private to that module. This implies that even though these elements are public, they are by default hidden and inaccessible from other modules.
And even forcefully, if you try to use public APIs located in a module’s nested subpackages (which Spring Modulith considers private by default), Spring Modulith will fail certain tests. This indicates that your code isn’t truly modular or that something is incorrect in your design with an appropriate message.
What sets Spring Modulith apart is its enforcement capabilities. It actively monitors and validates module interactions during development and testing.This ensures that modules only communicate through their intended public interfaces, preventing unwanted dependencies and maintaining architectural integrity.
Spring Modulith includes built-in support for testing modules in isolation, generating documentation about module relationships, and analyzing dependencies between different parts of your application. It also works well with event-driven architectures, supporting communication between modules through events.
💡 Note: Spring Modulith works particularly well with SQL databases.
Always remember that modular monolith is an architecture, and spring modulith is a technology used to implement that architecture.
Create a Spring Boot Maven project using start.spring.io with Java 21, and include the following dependencies:
pom.xml file should look something like this:
every package inside <span class="pink">com.unlogged.gadgetgarage</span>
(root package) is considered by default as a module by Spring Modulith.
.internal</span>
packages
💡 NotNote: To demonstrate how to organize a modular project structure, this project only includes the Order and Product modules. A full-fledged e-commerce application could introduce several additional modules, each thoughtfully designed around specific domains to enhance functionality and scalability.
Project modules structure
We have 2 modules: orders and product, and each module has an internal package and public APIs. The internal package is created for putting internal implementation classes such as domain, repository, and service classes that must be hidden from other modules. And the other classes, such as DTO and service interface, are used by external modules for module communication and method calling.
In a modular monolith using Spring Modulith:
Order.java
<span class="pink">Order Entity</span>: Represents an order in the system, containing a list of order items and a status field to track the order state.
OrderItem.java
<span class="pink">OrderItem Entity</span>: represents dividual items in an order, including the product ID and quantity.
OrderRepository.java
<span class="pink">OrderRepository</span>: A JPA repository interface for managing <span class="pink">Order</span> entities, providing CRUD operations.
OrderServiceImpl.java
<span class="pink">OrderServiceImpl</span>: Implements the business logic for order creation, retrieval, and mapping between entities and DTOs. It validates products via the <span class="pink">ProductService</span>.
OrderDto.java
<span class="pink">OrderDto</span>: A lightweight data transfer object to expose order details, including items and status.
OrderItemDto.java
<span class="pink">OrderItemDto</span>: A DTO to represent individual order item details like product ID and quantity.
OrderService.java
<span class="pink">OrderService</span> Interface: Defines the contract for order-related operations like creating and fetching orders.
Product.java
<span class="pink">Product</span> Entity: Represents a product including fields for name, description, and price.
ProductRepository.java
<span class="pink">ProductRepository</span>: A JPA repository interface for managing <span class="pink">Product</span> entities with basic CRUD capabilities.
ProductServiceImpl.java
<span class="pink">ProductServiceImpl</span>: Implements the product-related business logic, including creating, updating, and retrieving products.
ProductDto.java
<span class="pink">ProductDto</span>: A DTO to transfer product details between layers in a structured manner.
ProductService.java
<span class="pink">
ProductService
Interface</span>
This interface defines the contract for handling product-related operations within the <span class="pink">product</span>
module
GadgetGarageApplication.java
<span class="pink">
GadgetGarageApplication</span>: The main entry point of the Spring Boot application, annotated with <span class="pink">
@Modulith</span> to signify the use of Spring Modulith for modular architecture.
Here comes the arch unit to save the day, which is inbuilt in spring modulith.
<span class="pink">Spring Modulith builds upon the robust foundation of the ArchUnit library to enforce architectural boundaries and design principles.</span>
In the world of enterprise applications, maintaining architectural integrity becomes increasingly challenging as systems grow. While microservices offer clear physical boundaries, many organizations opt for modular monoliths as a pragmatic middle ground.
However, without proper enforcement, these modular boundaries can easily blur over time.
This is where ArchUnit comes to the rescue.
ArchUnit is a powerful library that tests architecture and coding rules using unit tests.
Key functionalities:
Output after running this test to check modularity between modules:
Tests passed mean that modules are moular in nature; no boundaries are crossed between modules.
Spring Modulith also provides a convenient way to generate architectural documentation through unit tests, automatically creating diagrams that visualize your application's modular structure. These tests examine your codebase and produce PlantUML diagrams showing both the overall module relationships and detailed views of individual modules, making it easier to understand and maintain your application's architecture.
We are creating a diagram of our Spring Boot project that includes the order and product modules.
First write this test in your main test class.
Explanation: This test code generates visual documentation for the GadgetGarage application.
When executed, it creates PlantUML diagrams that visualize the application's modular structure in two ways: first, it produces a comprehensive diagram showing all modules and their relationships with each other through the <span class="pink">writeModulesAsPlantUml()</span>
method, and second, it creates individual, detailed diagrams for each module using <span class="pink">writeIndividualModulesAsPlantUml()</span>
.
a folder named <span class="pink">spring-modulith-docs</span>
get generated inside <span class="pink">target</span>
folder within your root application folder, which contains puml files
opening the <span class="pink">
components.puml</span> files with the help of a puml plugin in IntelliJ
<span class="pink">package-info.java</span>
in Spring Modulith<span class="pink">package-info.java</span>
serves as the access control definition for Spring Modulith modules. It defines clear boundaries of what code can be accessed between different parts of your application.
Spring Modulith verifies these boundaries through tests:
This verification ensures no module oversteps its boundaries. It catches architectural violations early, preventing the gradual decay that often plagues large applications.<span class="pink">package-info.java</span>
transforms module organization from a suggestion into an enforced reality.
For example: When a new developer joins the team,<span class="pink">package-info.java</span>
serves as living documentation. It answers critical questions: What does this module do? What can it depend on? How should other modules interact with it?
These answers come not from outdated documentation but from the actual structure of the code.
The real value emerges in large applications. As systems grow, the clarity enforced by package-info.java prevents the chaos that often comes with scale. Modules remain focused and independent, making the system easier to understand, maintain, and evolve.
Suppose we have an application with several modules like <span class="pink">orders
, product
, payment
, inventory</span>
, and <span class="pink">shared</span>
. If we want the <span class="pink">orders</span>
module to communicate only with specific modules, we can explicitly define these dependencies in a <span class="pink">package-info.java</span>
file within the <span class="pink">orders</span>
module. To do this, create a <span class="pink">package-info.java</span>
file directly inside the root of the <span class="pink">orders</span>
module, and the code would look like this:
The <span class="pink">@ApplicationModule</span>
annotation marks the <span class="pink">orders</span>
package as a module and allows it to depend only on the <span class="pink">product</span>
module. If no modules are specified in <span class="pink">allowedDependencies</span>
, all public APIs from other modules in the application are accessible to the <span class="pink">orders</span>
module by default.
1. Root Package Visibility:When interfaces or classes are directly in a module's root package, they are automatically visible to other modules.
2. Subpackage Privacy Rule:When you create any subpackage (like 'api', 'internal', 'service'), everything inside becomes private to that module by default.
💡 The <span class="pink">'internal'</span>
package in Spring Modulith has special significance. It is reserved for truly private implementations.
3. Making Subpackages Public:
By default, Spring Modulith keeps everything in subpackages private for better encapsulation.
<span class="pink">But when you need to share certain interfaces or classes with other modules, you can use @NamedInterface to make specific subpackages accessible, while still maintaining clear boundaries and preventing messy dependencies.</span>
To make a subpackage accessible to other modules, you need to focus on certain things:
Example: If we modify our earlier project folder structure and move the <span class="pink">OrderService</span>
, <span class="pink">ProductService</span>
and other public APIs into sub-package named <span class="pink">api</span>
within the <span class="pink">orders</span>
and product
modules, respectively, by Spring Modulith's definition, all classes, interfaces, and other components inside the <span class="pink">api</span>
sub-package will be treated as private since it resides within the module's package.
The restructured folder should look like this:
When running the modularity test, the following error is displayed in the console, indicating a failure:
<span class="pink">org.springframework.modulith.core.Violations: - Module 'orders' depends on non-exposed type com.unlogged.gadgetgarage.product.api.ProductService within module 'product'!</span>
To resolve this issue, we create a file named <span class="pink">
package-info.java
</span> and include the below code. Since the orders module depends on the product module, we explicitly expose all the APIs within the <span class="pink">api</span>
package of the product module.
Explanation: This annotation (<span class="pink">@NamedInterface("ProductsAPI"</span>)
) marks the product.api
package as a public part of the product
module, meaning other modules can access its contents. It helps clearly define what is meant to be shared while keeping other parts of the module private.
This will fix the issue, and all the modularity tests will pass.
A cyclic dependency occurs when one module, say Module A, depends on another module, Module B, and in turn, Module B depends back on Module A.
In Spring modulith, this creates build failures with tools like Maven or Gradle because they can't determine which module to compile first.
The Spring framework also fails during runtime startup; it can't initialize these modules since each needs the other to exist first.
To fix this:
Cyclic dependencies indicate modules that are too tightly coupled—they tend to change together and are difficult to maintain separately.
For a quick example:
Adding just two lines of code in the <span class="pink">ProductServiceImpl</span>
class that even do not have any implementation creates a cyclic depedency between orders and the product module and fails the test.
The highlighted part in grey informs us that the orders module depends on the product module, and the product module depends on the orders module, which in turn creates a cyclic dependency and fails the modularity test.
The cycle is:
💡 orders -> product -> orders
<span class="pink">orders</span>
module depends on <span class="pink">product</span>
module (via ProductService
in <span class="pink">OrderServiceImpl</span>
)
product
module depends on orders
module (via OrderCreatedEvent
in ProductServiceImpl
)and thus cyclic dependency exists between the modules and build fails for Maven, Gradle, etc. So try to put the common code in a shared module and make it public or use async communication like events.
Modular monoliths and Spring Modulith are extensive topics that require weeks or even months of research and study to fully grasp. If this topic interests you and you're looking to get hands-on experience or transition an existing project from a monolith—or even return from microservices—feel free to dive in.
But this article provides more than enough to help you get started and operational. In upcoming articles, we’ll explore module communication using asynchronous events for a more loosely coupled system, discuss the concept of private tables for each module in the database, and delve into detailed testing strategies to ensure the robustness of your modular architecture.