We used Jigsaw less than expected, but I found an effective way to leverage it.
Project Jigsaw was released with JDK 9 back in 2017. At the time, it seemed like my team would use Java modules extensively, but in reality, we didn’t use them as much as we expected.
However, I discovered a way to make use of it.
Why does IO service routing have a Java module?
Back in 2021, Java 17 was released, featuring JEP 409: Sealed Classes. However, JEP 409 didn’t receive much attention as it was overshadowed by other JEPs.
As always, I’ve been tracking upcoming Java features, eagerly anticipating pattern-matching support. Sealed classes were crucial for effective pattern matching, which aimed to eliminate the Visitor pattern in Java, as explained in in JEP 441: Pattern Matching for switch. Although Pattern Matching was still in preview in Java 17, that didn’t stop me from trying it out.
A fundamental concept in the IO Service routing domain is a route. A route represents a link between the current service instance and the destination service instance. Different destination service instances can be in various states – they can be test instances of different kinds, production instances, or unconfigured instances. Each of these types is represented by a different implementation of the Route interface and is contained in a separate package.
Up until this point, the Route hierarchy used the Visitor pattern. To explore how pattern matching could eliminate the Visitor pattern, my first step was to make the Route hierarchy sealed. However, I encountered a significant limitation of Sealed Classes when not using Java modules:
The classes specified by permits must be located near the superclass: either in the same module (if the superclass is in a named module) or in the same package (if the superclass is in the unnamed module).
In other words, if my project was not a module, all classes needed to be in the same package. This might not seem like a big deal if you have only a few classes, but what if you have five or more?
The second problem was that I implemented the Package By Feature approach which meant that different route implementation packages also contained related repository and service classes. Merging all packages into one would create a package with a large number of classes.
This situation surprised me so I created a stack overflow question. Answers to the question only confirmed my realization and cemented my perspective on this feature:
Doesn’t this set a precedent where a feature on the module path is more flexible than on the classpath and that in turn – while the classpath is still supported, it’s not as a first-class citizen as it used to be when compared to the module path?
This was the first time I encountered a situation where a Java language feature was richer when using a module path compared to using the classpath.
Challenge accepted
So, I accepted the challenge and spent some time researching possible solutions to this issue.
The first realization was that only the domain model needed to be modularized. This led me to favor modularization over flattening the packages, as the only compromise required would be placing the domain model in a separate Maven module.
I had some suspicion on whether this would work overall but it turned out fine and this resulted in:
module infobip.io.service.routing.sealed.hierarchy {
requires static Java.compiler;
requires spring. data.commons;
requires infobip.io.service.routing.connector;
requires com.query.core;
requires com.querydsl.sql;
requires java.sql;
}
All those required statements were generated with assistance from my IDE so it wasn’t as bad as I thought it would be.
Widespread adoption
Since this commit is integrated into the IO service routing, it indicates that modular libraries are viable and not a stumbling block – with many already utilizing this approach.
Future outlook
While this example demonstrates that it can be done, its scope and usefulness are quite limited.
Still, the biggest takeaway for me was that the JDK team is ready to make new Java language features leverage and depend on Java modules, sacrificing the status quo of the majority of Java code still being written with the classpath in mind.
For pattern matching specifically, I would still weigh my options in the future. However, so far, there are very few downsides to modularizing the code. As long as there aren’t many dependencies in the module, it really shouldn’t be a problem, so I would consider it as an option.