Spring Modulith – An Architecture Guard

“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.

Leave a Comment