One of the main reasons to design microservices is that they enforce strong module boundaries. However, the cons of microservices are so huge that itâs like chopping off your right hand to learn to write with the left one; there are more manageable (and less painful!) ways to achieve the same result.
Even since the microservices craze started, some cooler heads have prevailed. In particular, Oliver Drotbohm, a developer on the Spring framework, has been a long-time proponent of the moduliths alternative. The idea is to keep a monolith but design it around modules.
Many flocks to microservices because the application they work on resembles a spaghetti platter. If their application were better designed, the pull of microservices wouldnât be so strong.
Why modularity?
Modularity is a way to reduce the impact of change on a codebase. Itâs very similar to how one designs (big) ships.
When water continuously leaks into a ship, the latter generally sinks because of the decreasing Archimedes thrust. To avoid a single leak sinking the ship, itâs designed around multiple watertight compartments. If one leak happens, itâs contained in a single compartment. While itâs not ideal, it prevents the ship from sinking, allowing it to reroute to the nearest port where one can repair it.
Modularity works similarly: it puts boundaries around parts of the code. This way, the effect of a change is limited to the part and doesnât spread beyond its boundaries.
In Java, such parts are known as packages. The parallel with ships stops there because packages must work together to achieve the desired results. Packages cannot be âwatertightâ. The Java language provides visibility modifiers to work across package boundaries. Interestingly, the most famous one, public
, allows crossing packages entirely.
Designing boundaries that follow the principle of least privilege is a constant effort. Chances are that under the projectâs pressure in the initial development or with time during maintenance, the effort will slip, and boundaries will decay.
We need a more advanced way to enforce boundaries.
Modules, modules everywhere
In the long history of Java, âmodulesâ have been a solution to enforce boundaries. The thing is, there are many definitions of what a module is, even today.
OSGI, started in 2000, aimed to provide versioned components that could be safely deployed and undeployed at runtime. It kept the JAR deployment unit but added metadata in its manifest. OSGi was powerful, but developing an OSGi bundle (the name for a module) was complex. Developers paid a higher development cost while the operation team enjoyed the deployment benefits. DevOps had yet to be born; it didnât make OSGi as popular as it could have been.
In parallel, Javaâs architects searched for their path to modularizing the JDK. The approach is much simpler compared to OSGI, as it avoids deployment and versioning concerns. Java modules, introduced in Java 9, limit themselves to the following data: a name, a public API, and dependencies to other modules.
Java modules worked well for the JDK but much less for applications because of a chicken-and-egg problem. To be helpful to applications, developers must modularize librariesââânot relying on auto-modules. But library developers would do it only if enough application developers would use it. Last time I checked, only half of 20 commons libraries were modularized.
On the build side, I need to cite Maven modules. They allow splitting oneâs code into multiple projects.
There are other module systems on the JVM, but these three are the most well-known.
A tentative approach to enforce boundaries
As mentioned above, microservices provide the ultimate boundary during development and deployment. They are overkill in most cases. On the other side, thereâs no denying that projects rot over time. Even the most beautifully crafted one, which values modularity, is bound to become a mess without constant care.
We need rules to enforce boundaries, and they need to be treated like tests: when tests fail, one must fix them. Likewise, when one breaks a rule, one must fix it. ArchUnit is a tool to create and enforce rules. One configures the rules and verifies them as tests. Unfortunately, the configuration is time-consuming and must constantly be maintained to provide value. Hereâs a snippet for a sample application following the Hexagonal architecture principle:
HexagonalArchitecture.boundedContext("io.reflectoring.buckpal.account")
.withDomainLayer("domain")
.withAdaptersLayer("adapter")
.incoming("in.web")
.outgoing("out.persistence")
.and()
.withApplicationLayer("application")
.services("service")
.incomingPorts("port.in")
.outgoingPorts("port.out")
.and()
.withConfiguration("configuration")
.check(new ClassFileImporter()
.importPackages("io.reflectoring.buckpal.."));
Note that the HexagonalArchitecture
class is a custom-made DSL façade over the ArchUnit API.
Overall, ArchUnit is better than nothing, but only marginally so. Its main benefit is automation via tests. It would significantly improve if the architectural rules could be automatically inferred. Thatâs the idea behind the Spring Modulith project.
Spring Modulith
Spring Modulith is the successor of Oliver Drotbohmâs Moduliths project (with a trailing S). It uses both ArchUnit and jMolecules. At the time of this writing, itâs experimental.
Spring Modulith allows:
- Documenting the relationships between the packages of a project
- Restricting certain relationships
- Testing the restrictions during in tests
It requires that oneâs application uses the Spring Framework: it leverages the latterâs understanding of the former, obtained through DI assembly.
By default, a Modulith module is a package located at the same level as the SpringBootApplication
-annotated class.
|_ ch.frankel.blog
|_ DummyApplication // 1
|_ packagex // 2
| |_ subpackagex // 3
|_ packagey // 2
|_ packagez // 2
|_ subpackagez // 3
- Application class
- Modulith module
- Not a module
By default, a module can access the content of any other module but cannot access
Spring Modulith offers to generate text-based diagrams based on PlantUML, with UML or C4 (default) skins. The generation is easy as pie:
var modules = ApplicationModules.of(DummyApplication.class);
new Documenter(modules).writeModulesAsPlantUml();
To break the build if a module accesses a regular package, call the verify()
method in a test.
var modules = ApplicationModules.of(DummyApplication.class).verify();
A sample to play with
Iâve created a sample app to play with: it emulates the home page of an online shop. The home page is generated server-side with Thymeleaf and displays catalog items and a newsfeed. The latter is also accessible via an HTTP API for client-side calls (that I was too lazy to code). Items are displayed with a price, thus requiring a pricing service.
Each featureâââpage, catalog, newsfeed, and pricingâââsits in a package, which is viewed as a Spring module. Spring Modulithâs documenting feature generates the following:
Letâs check the design of the pricing feature:
The current design has two issues:
- The
PricingRepository
is accessible outside of the module - The
PricingService
leaks thePricing
JPA entity
We shall fix the design by encapsulating types that shouldnât be exposed. We move the Pricing
and PricingRepository
types into an internal
subfolder of the pricing
module:
If we call the verify()
method, it throws and breaks the build because Pricing
is not accessible from outside the pricing
module:
Module 'home' depends on non-exposed type ch.frankel.blog.pricing.internal.Pricing within module 'pricing'!
Letâs fix the violations with the following changes:
Conclusion
By toying with a sample application, I did like Spring Modulith.
I can see two prominent use cases: documenting an existing application and keeping the design âcleanâ. The latter avoids the ârotâ effect of applications over time. This way, we can keep the design as intended and avoid the spaghetti effect.
The icing on the cake: itâs great when we need to chop one or more features to their deployment unit. It will be a very straightforward move, with no time wasted to untangle dependencies. Spring Modulith provides a huge benefit: delay every impactful architectural decision until the last possible moment.
Thanks, Oliver Drotbohm for his review.
You can find the source code on GitHub.
Originally published at A Java Geek on November 13th, 2022