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;
@org.springframework.modulith.ApplicationModule(displayName = "EventConsumers")
package com.giri.myapp.consumer;
Limitations
- Sub-package exposed as package cannot be shown in generated diagram.
- 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
- Spring Modulith GitHub Repo
- Reference Documentation
- API - Javadoc
- Java - package comment file
- The package-info.java File