“Architecture is not about space but about time”
Vito Acconci
Spring Modulith is now available in version 1.3.0. So it’s time to write an article about this framework. I have created many microservices with Spring Boot over the last few years, but of course that doesn’t mean that you can’t also create large, monolithic applications with the framework.
Monolithic applications, or monoliths for short, have a not undeservedly bad reputation. Without a clean architecture pattern, these applications sooner or later fall into the phase of unmaintainable legacy software. The problem is rarely that the monolith started without a clean architecture. Usually, the monolith only turns into the unloved monster through further development and maintenance.
Incidentally, it makes no difference whether the monolith starts with a layered architecture, an onion architecture, a hexagonal architecture or a modular monolith. Malicious or disinterested project participants can turn any architecture into legacy code, it just sometimes takes a little longer.
Spring Modulith is a Spring library that aims to support software developers in maintaining a clean architecture. It can be added to existing projects as an additional dependency.
Spring Modulith provides support in several ways.
Firstly, the library supports compliance with the architecture through special tests. The architecture is created by separating the code base into various modules. The correct access between the modules is then verified in the tests.
Secondly, the library supports event handling between the individual modules. In addition to the direct synchronous call of components of another module, Spring Modulith supports asynchronous, transactional event communication between the modules via Spring Application Events.
Thirdly, the library supports the testing of the individual modules via integration tests and the provision of scenarios.
And finally, Spring Modulith supports the developers in documenting the architecture.
This article will initially focus on the use of Spring Modulith to comply with the architecture. Later articles will deal with the other aspects of the library.
Compliance with the architecture can be checked with Spring Modulith using a simple unit test.
@Test void createApplicationModuleModel() { ApplicationModules modules = ApplicationModules.of(AdvertisingApp.class); assertEquals(3, modules.stream().count()); modules.verify(); }
In the first line, an ApplicationModules
instance is created that contains all the information about the modules of our AdvertisingApp
application. In the last line, the verify
method is called to check whether all modules comply with the architecture. The second line checks the number of modules to ensure that no major changes have been made to the architecture.
At this point, the question naturally arises as to how Spring Modulith knows the modules and how they must be defined in your own application. Fortunately, Spring Modulith follows the well-known Spring Boot philosophy of conventions over configuration at this point.
de.schegge.advertising AdvertisingApp customers Customer CustomerService products Product ProductService emails EmailService
Our Spring Boot application is located in the package de.schegge.advertising
and some other classes are located in the sub-packages customers
, products
and emails
. These sub-packages define the modules of our application without any further intervention by the developers. There are, of course, several options for developers to further modify the module structure. This is usually only necessary if an existing application is to be tested with Spring Modulith.
If we call up our architetural test for the application, we receive the following error message.
- Cycle detected: Slice customers -> Slice emails -> Slice customers
Spring Modulith has recognized a cyclic dependency between the modules. In this case, it is a dependency between the customer
and emails
modules.
@Service public class EmailService { // ... public void sendEmail(Customer customer, String productName) { // ... } }
The reason is obvious. The sendEmail
method of the EmailService
is called by the CustomerService
with a Customer
instance. However, the Customer
is defined in the customers
module and is not available in the emails
module.
We have identified a breach of the architecture here and now have several options for correcting this error. In the simple case of a legacy application with many violations, we can defer the problem for the time being and allow access to the Customer
class.
@ApplicationModule(type = ApplicationModule.Type.OPEN) package de.schegge.advertising.customers; import org.springframework.modulith.ApplicationModule;
A module can be modified with the annotation @ApplicationModule
in package-info.java
. In this case, we change the type of the module from CLOSED
to OPEN
. This opens the doors and all other modules can use the types from the module customers
.
As this is not a correction of an architectural violation, we are therefore immediately looking at a better solution.
@Service public class EmailService { // ... public void sendEmail(IndividualAd individualAd) { // ... } }
The EmailService
has been modified so that its sendEmail
method is passed an IndividualAd
instance, which is defined in the emails
module. In this case, there is no longer a cyclical dependency, as the EmailService
no longer accesses classes from the customers
module.
However, the previous concept still allows access between the modules, which is not desired. For example, if the products
module has another service that should only be used internally, CustomerService
can also access this service.
@Service public class ProductUpdateService { public Product update(UUID uuid, ProductUpdate update) { return new Product(update.label(), uuid); } }
As the ProductUpdateService
is defined in the products
module, the CustomerService
can also access it and the ProductUpdate
class. A simple solution in this case is to move the classes from the package de.schegge.advertising.products
to the sub-package de.schegge.advertising.products.intern
. Sub-packages of modules are internal by convention and no other module can access types in this package.
The next test call now leads to an error, as the CustomerService
can no longer access the classes.
- Module 'customers' depends on non-exposed type de.schegge.advertising.products.internal.ProductUpdate within module 'products'!
Another configuration for modules should not be missing in an introduction to Spring Modulith. In our example, all modules can still access all accessible services of the other modules. No real help for a good architecture, whether layered, onion or hexagonal architecture.
Let’s add a billing
module to our example. However, we do not want our customers
module to access the billing
module. We can realize this by specifying all permitted dependencies for the customers
module.
@ApplicationModule(allowedDependencies = {"products", "emails"}) package de.schegge.advertising.customers; import org.springframework.modulith.ApplicationModule;
If an attempt is now made to use types from billing
, our architecture test again returns an error.
Also the emails
module should not be able to access any other module; this can also be defined with the @ApplicationModule
annotation.
@ApplicationModule(allowedDependencies = {}) package de.schegge.advertising.emails; import org.springframework.modulith.ApplicationModule;
The empty list now ensures that any use of another module in the emails
module leads to a test error.
Spring Modulith provides a few more options for modifying the module architecture of your own application, but the options presented here should be sufficient for a first impression.
Will Spring Monolith save us from the architectural decay of our applications? Certainly not on its own, because the biggest enemies of good architecture are quickly integrated features and bug fixes. Like any other Software Architecture As Code approach, Spring Modulith can be neutralized by those who write the legacy code.