javagradlearchitecturearchunit

How to restrict visibility between Gradle modules in a modular monolith Spring Boot application


I'm working on a large modular monolith Spring Boot application (Java) that's structured into multiple Gradle build modules. Each module represents a specific domain (e.g., user management, notification handling), and contains its own internal business logic that ideally should remain private and inaccessible from other domains.

To enforce this encapsulation, I've structured each domain module into two sub-modules: api and impl. Using Gradle plugins, I’ve defined rules such that only api sub-modules can be used as dependencies by other modules. This approach generally works, but it comes with two significant drawbacks:

Increased Module Count: Every domain now has two modules (api and impl), which essentially doubles the number of Gradle modules and increases project complexity.

Dependency Cycles: It’s still possible to accidentally introduce cycles—for example, moduleA:impl depending on moduleB:api, and moduleB:impl depending on moduleA:api. These cycles are hard to detect and can easily slip through code reviews.

I’m looking for better tooling or practices to:

One idea I’ve considered is using ArchUnit tests to assert architectural boundaries. For instance, moduleA could declare it’s only allowed to access the api package of moduleB. However, this raises a few concerns:

Each module would need to explicitly define what it’s allowed to see, which could be error-prone if a developer forgets to add the appropriate rule.

I'm unsure whether ArchUnit can be configured in a way that allows these rules to be enforced globally across the entire project.

My question is: Are there tools, best practices, or Gradle configurations that can help enforce strict module boundaries and encapsulation in a multi-module Java project?

Any advice or examples would be greatly appreciated.


Solution

  • I was going to comment, but for readability this will be easier as an answer. I'm not sure this qualifies as an answer as I am not familiar with Gradle.

    Regarding:

    Are there tools, best practices ... that can help enforce strict module boundaries and encapsulation

    ...and keeping in mind:

    I've structured each domain module into two sub-modules: api and impl.

    This answer https://stackoverflow.com/a/62257045/39094 talks about access with regards to modules. On the basis of that I'd have thought you can have one module instead of two for a given domain, and just have it so only the API methods are set as public / accessible on the module. That way encapsulation can be enforced as you want it, and you have less modules and a simpler design.

    Regarding Dependency Cycles. @SpaceTrucker makes an excellent point - just because calls go in both directions doesn't mean it's necessarily cyclical as in a death spiral, but it does indicate some potential coupling or at least that they work closely together to achieve something. Might be worth a design review, but doesn't mean you should panic right off the bat.

    Some kind of static code review would probably be a good way to test for the presence of stuff like that, but I have not used static code analysis before so can't comment further. It looks like there's no shortage of tooling options for Java in that regard.

    Internet search informs me that yes, you can use Gradle to control/initiate static code review - so yes it's possible, and based on my generic software engineering experience such an approach would not be an objectively bad idea.