VMware has introduced an experimental project, Spring Modulith, to better structure monolithic Spring Boot 3 applications through modules and events. The project introduces new classes and annotations but doesn't generate code. Its modules don't use the Java Platform Module System (JPMS), but instead map to plain Java packages. Modules have an API, but Spring Modulith encourages using Spring application events as the "primary means of interaction." These events can be automatically persisted to an event log. Spring Modulith also eases the testing of modules and events.
The upcoming Spring Boot 3 framework, due in November 2022, is the foundation of Spring Modulith. So it has a baseline of Spring Framework 6, Java 17, and Jakarta EE 9. Spring Modulith is the successor of the Moduliths (with a trailing "s") project. That project used Spring Boot 2.7 and is now retired, receiving only bug fixes until November 2023.
Spring Modulith introduces its module abstraction because Java packages are not hierarchical. That's why in this sample code below, the SomethingOrderInternal
class from the example.order.internal
package is visible to all other classes, not just the ones from the example.order
package:
Example
└─ src/main/java
├─ example
| └─ Application.java
├─ example.inventory
| ├─ InventoryManagement.java
| └─ SomethingInventoryInternal.java
├─ example.order
| └─ OrderManagement.java
└─ example.order.internal
└─ SomethingOrderInternal.java
Now Spring Modulith can't make Java compilation fail for violation of its module access rules. It uses unit tests instead: ApplicationModules.of(Application.class).verify()
fails for the example above if another module tries to access the module-internal class SomethingOrderInternal
. Spring Modulith relies on the ArchUnit project for this capability.
Spring Modulith encourages using Spring Framework application events for inter-module communication. It enhances these events with an Event Publication Registry which guarantees delivery by persisting events. Even if the entire application crashes, or just a module receiving the event does, the registry still delivers the event. The registry supports different serialization formats and defaults to JSON. The out-of-the-box persistence methods are JPA, JDBC, and MongoDB.
Testing events is also improved: This example demonstrates how the new PublishedEvents
abstraction helps filter received events to OrderCompleted
with a particular ID:
@Test
void publishesOrderCompletion(PublishedEvents events) {
var reference = new Order();
orders.complete(reference);
var matchingMapped = events
.ofType(OrderCompleted.class)
.matchingMapped
(OrderCompleted::getOrderId,
reference.getId()::equals);
assertThat(matchingMapped).hasSize(1);
}
Spring Modulith can automatically publish events like HourHasPassed
, DayHasPassed
, and WeekHasPassed
at the end of a particular duration (such as an hour, day, or week). These central Passage of Time events are a convenient alternative to duplicated Spring @Scheduled
annotations with cron
triggers in the modules.
Spring Modulith does not include a workflow, choreography, or orchestration component for coordinating events, as the Spring ecosystem offers plenty of choices there.
Spring Modulith uses the new observability support of Spring Framework 6 to automatically create Micrometer spans for module API durations and event processing. Spring Modulith can also document modules by creating two kinds of AsciiDoc files: C4 and UML component diagrams for the relationship between modules and a so-called Application Module Canvas for the content of a single module, such as Spring beans and events.
InfoQ spoke to Spring Modulith project lead Oliver Drotbohm, Spring Staff 2 engineer at VMware.
InfoQ: Microservices solve organizational issues of monoliths, such as the failure of various departments to ship at the same release cadence. They also have technical advantages, such as the ability to scale application parts independently and use different technology stacks. Why did you decide then to improve monoliths? And why now?
Oliver Drotbohm: Microservices architectures are very well covered by the Spring Cloud projects. However, we do not want teams to feel nudged into a particular architectural style just because the technological platform supports it better in one way or another. We want our users to feel equally supported, independent of what architecture they decide to use.
That said, monolithic systems, but also individual elements of a distributed system, have some internal structure. At best, the structure evolves and changes over the lifetime of the overall system. Our goal should be that, at worst, it at least does not accidently degrade. Spring Modulith helps to express and verify structure within a single Spring Boot application: verifying that no architectural violations have been introduced, integration testing modules in isolation, runtime observability of the modules' interactions, extracted documentation, etc.
Timing is a good point, though. We have seen a heavy trend to distribute systems until roundabout three years ago. Practical experience has shown that teams often over-divided their systems. Starting with a slightly more modulithic arrangement has its benefits, especially in domains evolving significantly: the module arrangement needs to change more rapidly as more insight into business requirements is gained. That is much easier to achieve in a monolithic application. That is what let us see increased, revived interest in how to implement modular structures in applications.
InfoQ: How useful is Spring Modulith in an application where there would be only one module?
Drotbohm: I have yet to see a non-trivial piece of software that does something useful and doesn't bear some internal structure that warrants more than one logical module.
InfoQ: There are existing systems for structuring monoliths, such as Domain-Driven Design (DDD) or Hexagonal Architecture. It seems Spring Modulith created a new approach. Why?
Drotbohm: It does not necessarily create a new approach. We piggyback on the notion of a module that has had fundamental semantics for ages but can also be found in DDD as a means to structure Bounded Contexts. The question Spring Modulith wants to answer is how developers can non-invasively express these domain modules in application code. The expressed structure allows the framework to be helpful, in integration testing, in being able to observe the application, etc. Technical structuring approaches such as Onion and Hexagonal Architecture can also be applied to the modules, but rather act as an implementation detail. We want the domain to be the primary driver of the overall code arrangement, just as suggested by Dan North.
InfoQ: The goal of the Java Platform Module System (JPMS) in Java 9 was to provide "reliable configuration" and "strong encapsulation" to Java. Why did JPMS not meet your requirements for modules?
Drotbohm: The JPMS was designed to modularize the JDK, and it does an impressive job at that. That said, a few design decisions are quite invasive for application developers that would simply like to define a few logical modules within their Spring Boot app. For instance, JPMS requires each module to be a single JAR, and integration tests must be packaged as a separate module. This imposes severe technical overhead, especially if a much simpler approach can do the trick.
That said, Spring Modulith works fine in JPMS-structured projects. If your project benefits from the advanced technical separation of JPMS modules, by all means, go for it. We still add a few exciting features on top, like the ability to run integration tests of different scopes (standalone or an entire subtree of modules).
InfoQ: How do modules in Spring Modulith compare with bounded contexts from DDD?
Drotbohm: Within DDD, a module is a means of structure within a Bounded Context. In a microservice architecture, in which a context is often aligned with a deployable service, that might result in the individual Spring Boot application consisting of a couple of modules. In a more monolithic application, developers often leverage the stronger coupling between modules induced by the type system to their benefit. It allows them to use refactoring tools to change the overall arrangement and deploy the changes as a whole without a complex API evolution process. But even in those arrangements, Bounded Contexts can be established by loosening the coupling, introducing anti-corruption and mapping layers, etc. That said, the primary concept we attach to is the — as we call it — Application Module, independent of at which level developers apply Bounded Contexts to their application.
InfoQ: Modules in Spring Modulith expose an API to other modules. But they can also interact through so-called "application events," which the documentation suggests as "their primary means of interaction." Why does Spring Modulith prefer events?
Drotbohm: There are a couple of effects of the switch from invoking a Spring bean of another module to publishing an application event instead. First, it frees the caller from having to know about the parties that need to be invoked. This creates a dependency on the caller component, as the number of foreign beans to be injected increases. The primary problem this causes is that those foreign beans need to be available when we try to integration test the calling component. Of course, we can mock the collaborators, but that means that both the implementation and the tests need intimate knowledge about the arrangement, which methods are called, etc. Every additional component that would need to be called adds more complexity to the arrangement. Alternatively, we can deploy the system as a whole, which makes the tests brittle as all modules have to be bootstrapped, and an issue in module A can cause the tests for module B to fail.
Publishing an application event instead solves that problem, as it frees the publishing component from having to know who is supposed to be invoked and those components not even having to be available at integration test time. This is a key ingredient to the ability to test application modules in isolation. This is quite similar to using message publication as a means to integrate a distributed system instead of actively invoking related systems. Except that no additional infrastructure is needed as Spring Framework already provides an in-process event bus.
InfoQ: Other frameworks have various degrees of code generation. For instance, Angular has customizable schematics to generate small amounts of code, such as modules or components. What are the plans for code generation in Spring Modulith?
Drotbohm: None, except the already existing feature to create C4 and UML component diagrams from the structural arrangement.
InfoQ: How can I migrate an existing Spring Boot 3 project to Spring Modulith?
Drotbohm: We have taken much care to ensure that using the fundamental features of Spring Modulith's is as non-invasive as possible. In its most rudimentary form, and assuming you already follow the default package arrangement conventions, you would not even have to touch your production code. You could add the verification libraries to your project in the test scope and apply a prepared architectural fitness function in a test case.
InfoQ: Spring Modulith is an experimental project. How safe is it to use in production?
Drotbohm: Spring Modulith has a predecessor named Moduliths that is currently available in version 1.3 and has been used in production by a couple of projects for the last two years. Thus, the experimental status reflects the fact that we simply start new Spring projects as such. Also, compared to Moduliths, we flipped a couple of defaults and would like to see how the community reacts to those changes. We want to react to feedback rather quickly and avoid being limited by internal API compatibility requirements that we have to fulfill as a non-experimental Spring project for a while. The rough plan is to use the time until Spring Boot 3.1 to gather feedback and, unless we find any significant problem, promote the project to non-experimental in early Q2 2023.
InfoQ: Spring Modulith is currently at version 0.1 M2. What are the plans for its future?
Drotbohm: We are currently introducing the project to Spring developers, gathering feedback, and trying to incorporate this until the 1.0 release. Compared to Modulith, we have already added JDBC- and MongoDB-based implementations of the Event Publication Registry. We are looking into similar extensions of the current feature set, like more advanced observability features to capture business-relevant metrics per module or a visual representation of event-command flows through the application. It would be nice if, in a couple of years, we find the conventions established by Spring Modulith in as many Spring Boot applications as possible, no matter which architectural style they follow.
The project has already reached its second milestone of version 0.1. More details may be found in the documentation and source code on GitHub.