Most applications in the real world will accumulate a large amount of features and code in the long run. Multi-module projects are a good approach to structuring the application without having to go down the complex path of microservices. The following five tips can help to better organize such Spring Boot projects in the long run.
#1 Find a Proper Module Structure
In general, the use of two modules, “base” and “web,” is a good starting point for Spring Boot applications. The “base” module describes the basic setup, for example, database settings, and provides utility classes. Standards defined here then apply to all further modules. In “web,” all modules are combined, and the executable application is built — our executable “fat jar.”
The structure of the intermediate modules should be less technical and more domain-driven. First, here is a negative example of partitioning an online store.
Poor example of technical structuring
While a purely technical separation is possible, it offers little advantage: for a developer to perform a typical task (“add a filter to the search”), all the artifacts involved must be gathered from multiple modules. With a domain-driven subdivision, however, customization would be limited to one module for many tasks.
Good example of domain-driven modularization
When the application is extended over time, it is easy to add more modules with a domain-driven structure, e.g., “blog.” With a technical separation, this makes little sense, and at some point, you will end up with a big ball of mud.
#2 Minimize Dependencies
Each module should contain all artifacts that it needs to provide its own functionality in order to minimize dependencies on other modules. This includes classes, templates, dependencies, resources, and so on.
An interesting question is whether the ORM layer should be stored in a central module or split among the respective modules. Nowadays, the support for separation is very good, so that actually nothing speaks against it. Each module can contain its own JPA entities and Flyway / Liquibase changelogs. When running an integration test within a module, a partial database schema would be created based on the current and all referenced modules.
A useful library that supports separating the modules is Springify Multiconfig. It allows each module to contain its own
application-mc-xx.yml file to store specific configuration of that module. Without this library, the entire config must be in the
application.yml of the “base” module.
#3 Continuous Improvement
A bad architecture is better than none, but the best structure of the modules is never clear from the beginning. Since all dependencies are within one application, the partitioning can be adjusted with a single pull request if necessary. With microservices, an extensive, multi-stage procedure would usually be required.
It is always worthwhile to question the current status and further improve it in the long term.
#4 Gradle API vs. Implementation
While developing the code of a module, the directly and indirectly referenced modules of the current project are always available. When external libraries are declared as a dependency, there are two possibilities in Gradle: with
compile) the library is also visible indirectly, whereas with
implementation a library is only available within the current module. In other words, if a module is referenced and a library there is included with
implementation, it will be hidden and cannot be used in the code.
In the final app, all dependencies end up in the single “fat jar” again, but during the build process, invalid accesses would already be prevented. Following this approach, the libraries available during development should remain cleaner.
#5 Use Separate Test Jars
Each module has its own tests to validate the functionality it contains. Sometimes, however, test data is created, or utility methods are provided that are needed in the modules that build on top of them — for example, to create test users.
In general, classes and data that are only needed for tests should not be contained in the application that is later running in production. For Maven, Gradle, and Kotlin Script, there is a way to create separate test jars for each module. These can then be referenced specifically for the test code. More backgrounds on the implementation are here for Maven and here for Gradle.
Multi-module projects are getting increased attention right now, as microservices come with many challenges in practice, and the extra effort is only really worth it for larger development teams. By following best practices, well-structured applications can be developed for the real world. If later on you want to switch to microservices, individual modules can be broken out as standalone apps.