Thursday, November 14, 2024

Modularize Spring Boot micro-service with Spring Modulith - Notes from my exploration . . .

The dictionary meaning of Modularity is - the use of individually distinct functional units, as in assembling an electronic or mechanical system. In other words, it is the degree to which components of a system can be separated. 

In Software Development, achieving modularity involves structuring various components involved into distinct modules or packages. The separation of components of various concerns is where Software Developers often fall into the traps of architectural layers. In other words, structure code that aligns with familiar technology layers like controller, service, persistence, domain etc by grouping similar kind of classes into a package with the name that mainly describes the logical layer. Structuring code by architectural layers that gives an architectural layered overview was once a good practice, but is quite common and a norm in modern application development. With this kind of code packaging structure, code within one package or layer gets interwoven with many functional/business use-cases. This also achieves modularity, but gets driven by the technical architecture, and not driven by business domain use-cases. The top-level domain or functional visibility is lost and pushed underneath architectural layers.

Spring Modulith

Spring Modulith is fairly new addition to Spring Projects. It not only helps bring in well-structured application modules that can be driven by domain, but also helps in verifying their arrangement, and even facilitates creating documentation.

It comes with few key fundamental arrangement/accessibility principles/rules/conventions with some limitations. The arrangement rules are considered violated only if you have a verification test in place. Otherwise, with your own arrangement, you will be able to generate documentation like PlantUML diagram showing Modules, their dependencies and boundaries.

Some points learned and noted

  • Modulith modules are analogous to Java packages.
  • Without making any modular/structural changes, if you add Modulith dependency and a unit test-case to verify, it will detect and report circular references which is very useful by itself.
  • By default each direct sub-package of the application main package is considered an application module, a.k.a module's base package.
  • Each module's base package is treated as an API package with all public classes under a module's base package made available for access and dependency injection from other modules.
  • Sub-packages under a module's base package are treated as internal to that and are not accessible by any other module, though Java allows if classes under that sub-package are public.
  • In order to expose or make a sub-package under a module's base package accessible to other packages, you need to provide a package-info.java file under that sub-package by annotating that package with @NamedInetrface in that file. With this the annotated sub-package which otherwise is treated as internal to the module that is under becomes available and is accessible from other application modules.
  • The file package-info.java is Java's standard way of adding package documentation which was introduced in Java 5. This file can contain Javadoc comment with Javadoc tags for the package along with package declaration and package annotations. Spring framework's null safety annotations like @NonNullFields, @NonNullApi can be specified in this file with which those specified annotations can be applied to all classes under that package.
  • By default the name of a module in UML generated is nothing but the module's base package name with the uppercase first letter. This name can be customized by annotating the package with @ApplicationModule and specifying the value for displayName property. This applies to only base package of the module. Sub-packages cannot be shown in the generated UML diagram anyway.
  • Additional customizations are possible. But I wouldn't overuse as it defeats simplicity.  
An example of package-info.java in a sub-package made accessible to other modules is described below:

Application main package: com.giri.myapp
Application module base package: event
Sub-package of module events exposed to other modules: publishers
The file package-info.java under com.giri.myapp.event.publisher looks like below: 
package-info.java
@org.springframework.modulith.NamedInterface package com.giri.myapp.event.publisher;

An example of package-info.java added to a module (base package) with different module name (EventConsumers) than the default name (Consumer) is shown below:
@org.springframework.modulith.ApplicationModule(displayName = "EventConsumers") package com.giri.myapp.consumer;

Limitations

  • Sub-package exposed as package cannot be shown in generated diagram.
Documentation can be generated by having a simple test-case as outlined in the documentation. UML diagram is particularly useful to get to see Architectural overview of the application modules, their dependencies and boundaries. However, if you organized any of the related components into sub-packages of a module base package and exposed those by annotating the package by adding package-info.java as shown above, it's quite natural to expect that the sub-package is shown in the generated UML as a module since it's treated as a module from exposure point of view for other modules. But the UML diagram seems only restricted and limited to showing the default application modules, the direct sub-packages under the project main package.

I did explore the API little bit to find out if there is a way to override this rule by any means, I couldn't find a way to do so. Hope the future version will consider this kind of expectation and provide an option for specifying for documentation as the sub-package exposed as a module by annotating the package with @NamedInterface is visible and used/depended on by other modules.
  • Generated visible diagram files should be treated as code to be checked in.
The clickable modules and visual diagram generated as .puml files are only useful to developers with IDE plugins. A mechanism to integrate into code documentation files like README.md or wiki would be more useful to let the Visible Architecture up-to-date with the codebase.
 

TIP

IntelliJ IDEA has a PlantUML Integration plugin available and can be used to view generated .puml files as UML diagram in the IDE. Follow these steps in order to generate Spring Modulith modules diagram and view the UML diagram in IntelliJ IDEA.
  • Make sure you have graphviz installed.
  • Install IntelliJ IDEA's PlantUML Integration plugin
  • Run test-case: ModularityTest which generates modulith PlantUML files (.puml) under application's target/spring-modulith-docs directory.
  • When you open any .puml file generated in IntelliJ, the plugin shows it as PlantUML diagram.
A sample JUnit test-case (ModularityTest.java) is shown below:
package com.giri.myapp.modularity; import com.giri.myapp.MyApplication; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; class ModularityTest { ApplicationModules modules = ApplicationModules.of(MyApplication.class); /** * Test to verify application structure. Rejects cyclic dependencies and access to internal types. */ @Test void verifyModularity() { System.out.println(modules); modules.verify(); } /** * Test to generate Application Module Component diagrams under target/spring-modulith-docs. */ @SuppressWarnings("squid:S2699") @Test void writeDocumentationSnippets() { new Documenter(modules) .writeModulesAsPlantUml() .writeIndividualModulesAsPlantUml(); } }

Summary

Structuring by domain use-cases vs architectural layers cannot be a personal preference. Viewing an application from domain aspect with underneath familiar technological layers gives a view that aligns better with business than viewing an application from technological layers. Spring Modulith helps achieve domain-driven modularity by following simple package level modular conventions where modules align with domain concepts.

Achieving better modularity doesn't necessarily need to be showing only business domain-based modules. Certain aspects of technologies like GraphQL let's say to indicate that the application provides GraphQL API for its client can as well get depicted in the generated UML module diagram by structuring controllers into a module with name graphql. With a right balanced mix of modularity, code can be restructured to show all main business/domain use-cases along with some technology related modules like Rest, GraphQL, Events etc. added to the mix. This kind of balanced approach gives good architectural and structural overview of the application showing how modules interact with each other, in some cases event showing high-level flow.

Spring Modulith comes with added support for structural validation, visual documentation of the modular arrangement, modular testability and observability.

Domain-driven modularity brings in more maintainable and understandable structure. However, keep things simple and do not overuse modularity, it defeats simplicity that Software Development is badly in need of. ;)

References