Java Module System in 2026: Still Ignored, Still Relevant
JPMS shipped with Java 9 in 2017. Nearly a decade later, enterprise adoption remains stubbornly low. This is not a tutorial. It’s an honest reckoning with why a well-designed feature failed to win over the people it was built for — and what it quietly does well anyway.
1. Where JPMS Came From
The story of the Java Platform Module System is, first and foremost, a story of patience — or depending on your perspective, delay. The idea was first proposed as JSR 277 and was intended to ship with Java 7 in 2011. It was deferred to Java 8. Then deferred again. Project Jigsaw, as it was known internally, eventually landed in Java 9 in September 2017. By that point, the Java community had been hearing about modularisation for six years.
That prolonged development period matters for understanding why adoption went the way it did. By 2017, the ecosystem had already spent a decade solving the same problems through other means — Maven, Gradle, OSGi for those who needed real modularity, and a community-wide convention of simply living with the classpath. When JPMS finally arrived, it wasn’t entering a vacuum. It was competing with habits that were deeply embedded.
Nevertheless, the technical motivation behind JPMS was completely legitimate. Anyone who had spent time debugging classpath conflicts, fighting JAR hell, or trying to figure out which of twelve versions of a class was actually being loaded at runtime knew exactly what problem the module system was trying to solve. The question was never whether the problem was real. The question was whether the solution arrived at the right time and in the right form.
2. What It Was Supposed to Fix
JPMS had four clear goals when it shipped, and they’re worth restating plainly because they tend to get lost in tutorial-land:
| Problem | What JPMS offered | Real-world severity (pre-Java 9) |
|---|---|---|
| Classpath / JAR hell | Module path with explicit requires declarations | High — version conflicts were endemic in large projects |
| Broken encapsulation | Only explicitly exportsed packages are visible | Medium — internal APIs were routinely depended upon |
| Monolithic JDK | jlink to build trimmed custom runtimes | High for IoT/embedded; low for most enterprise apps |
| Security surface | Unexported packages inaccessible by default | Medium — many vulnerabilities exploited sun.* internals |
Notably, JPMS does not solve versioning. You cannot specify version constraints in a module-info.java file. There is no concept of version ranges. Maven and Gradle still handle that entirely. This omission became one of the most frequently cited criticisms, because it meant developers still needed their existing build tooling for the thing that caused them the most pain, while JPMS added a new layer on top of it.
The versioning gap
As InfoQ noted early on, JPMS modules can only declare requires anotherModule by name — not by version range. There is no equivalent of requires com.google.guava >= 31.0. External tooling must handle all of that. This means JPMS and Maven/Gradle are not interchangeable; they co-exist at different layers, adding complexity rather than removing it.
3. Why Nobody Adopted It
This is the part that tends to get glossed over, so let’s be direct about each reason separately, because they’re different in character.
3.1 The migration cost is enormous for existing projects
A project cannot become fully modular until every single dependency in its graph is also modular. That’s a hard constraint. As Stephen Colebourne documented early on, even a small, well-maintained open source library faces a thorny situation: adding module-info.java can break users who consume the JAR on the classpath rather than the module path, and older tooling using bytecode libraries like ASM may fail entirely on modular JARs. You end up having to ship two variants of your library — something which, as Colebourne put it, is “profoundly depressing.”
Furthermore, the classpath and module path behave differently in important ways. Code that compiles and runs on the classpath will often not compile or run on the module path, and vice versa. As a library author, you cannot control which environment your consumers use. That bifurcation is real friction, and it discourages migration rather than encouraging it.
3.2 The ecosystem has been slow to catch up
As of 2025, major libraries have largely either fully modularised or at least declared an Automatic-Module-Name. However, a significant portion of mid-tier and long-tail libraries on Maven Central remain non-modular. The automatic modules mechanism was a clever stop-gap that prevented JPMS from being dead on arrival. But it only goes so far. jlink, which builds lean custom runtimes, requires the entire dependency graph to consist of explicit modules. Automatic modules block it entirely. So the payoff of JPMS’s most compelling feature is locked behind full adoption the ecosystem hasn’t reached.
3.3 The mental model is genuinely harder than it looks
Writing a module-info.java file is not, on its own, difficult. The cognitive overhead comes from understanding what happens when you mix explicit modules, automatic modules, and classpath JARs in the same application. The visibility rules are more complex than the pre-Java 9 world. And the error messages when something goes wrong — particularly around reflective access — have historically been opaque. An InaccessibleObjectException at runtime because a framework tries to reflect on an unexported package is genuinely confusing the first time, and the fix of adding an --add-opens flag feels like punching a deliberate hole in the thing you just built.
4. The Spring Problem
This deserves its own section because Spring is not just a framework in the Java ecosystem — it is, for most enterprise Java shops, the entirety of the framework layer. The relationship between Spring and JPMS explains more about the adoption problem than anything else.
Spring Framework is built on reflection. Dependency injection, bean instantiation, AOP proxies — almost all of Spring’s core machinery relies on the ability to inspect and manipulate objects reflectively at runtime. JPMS’s central promise — that unexported packages are inaccessible — is in direct tension with that model. When Spring Framework 6.0 shipped in late 2022, the team was explicit: full JPMS module support was deprioritised in favour of GraalVM native image support. As Oliver Drotbohm confirmed at JAX London that year, “There have been very few requests for it in the course of this year.”
“Looking forward, the use of jlink’s module-bounded approach for application/framework-level modules might get superseded by runtime images based on GraalVM-style individual reachability analysis.”— Oliver Drotbohm, Spring Framework lead, JAX London 2022 · InfoQ, October 2022
That statement is significant. What it effectively says is: GraalVM may render the jlink/JPMS runtime-trimming use case irrelevant for Spring applications, because GraalVM’s reachability analysis is more powerful and doesn’t depend on full modularisation. If that’s true — and the evidence from Spring Boot 3’s native image support suggests it’s becoming true — then the argument for full JPMS adoption in Spring-based applications weakens further.
Consequently, if you’re a Spring developer today, you may never need to write a module-info.java for your application. Not because JPMS is wrong, but because the framework has charted a different course to the same destination.
5. The OSGi Shadow
It’s worth acknowledging that Java already had a module system before JPMS. OSGi has existed since 1999 and is used in production at scale by Eclipse, IntelliJ IDEA, and various industrial IoT platforms. In several meaningful ways, OSGi is more powerful than JPMS: it supports versioning with version ranges, it allows bundles to be installed, started, stopped, and uninstalled at runtime without restarting the JVM, and its service model is significantly more sophisticated.
The honest assessment of OSGi is that it solves niche problems most teams don’t have. Its learning curve is steep, its community is narrow, and its incompatibility with Spring Boot is a dealbreaker for most enterprise shops. But OSGi’s existence meant that the teams who really needed runtime modularity already had a solution. Everyone else was either happy with Maven or moving toward microservices, where the unit of isolation is the JVM process rather than a module within a JVM.
| Feature | JPMS | OSGi |
|---|---|---|
| Versioning in descriptors | No — external tooling only | Yes — full version range support |
| Runtime install / uninstall | No — static at startup | Yes — bundles hot-swap at runtime |
| JDK integration | Native — part of the platform | External container required |
| Spring compatibility | Partial (improving slowly) | Largely incompatible |
| Learning curve | Moderate | High |
| Ecosystem breadth | Growing — all modern JDK | Narrow — Eclipse, Karaf, IoT |
| jlink support | Yes — primary use case | No |
6. Where It Actually Works
Here’s where the post-mortem turns less gloomy. JPMS is not a failure in every sense. It is well-deployed in some specific contexts, and those contexts are growing.
6.1 The JDK itself
The most important modularisation JPMS has achieved is the one most developers never directly interact with: the JDK. The JDK is now a fully modular system. java.base, java.sql, java.desktop, and dozens of other platform modules are formally isolated from one another. Internal APIs like sun.misc.Unsafe are now in unexported packages, and the JVM issues warnings — and increasingly, hard errors — when they’re accessed. This is a genuine improvement in platform hygiene, regardless of whether application developers ever touch a module-info.java.
6.2 jlink and custom runtime images
This is arguably the most underused practical benefit of JPMS. When your application and all its dependencies are explicit modules, jlink lets you build a stripped-down JRE containing only the platform modules your code actually needs. The numbers are meaningful: using jlink on a real web server application delivered an immediate 49 MB Docker image reduction, before touching GraalVM at all. For teams that haven’t yet invested in native image compilation but still care about container footprint, jlink is a practical win worth exploring.
shell — jlink: build a minimal JRE for a modular app
# Step 1: discover which JDK modules your app actually needs
jdeps \
--ignore-missing-deps \
--recursive \
--multi-release 21 \
--print-module-deps \
--class-path "$(cat cp.txt)" \
target/myapp.jar
# Step 2: build the custom runtime with only those modules
jlink \
--module-path "${JAVA_HOME}/jmods" \
--add-modules java.base,java.net.http,java.sql \
--strip-debug \
--compress zip-6 \
--no-header-files \
--no-man-pages \
--output ./custom-jre
The result is a self-contained runtime directory you can ship in a Docker image without installing a full JDK. For a simple service, the custom runtime can come in under 40 MB — not GraalVM-level startup time, but also not the 250+ MB JRE you’d otherwise carry.
6.3 GraalVM native image compilation
As of 2025, one of the clearest growth areas for JPMS is as an input to GraalVM’s ahead-of-time compilation. A fully modular application gives the GraalVM compiler better information for its reachability analysis — the process that determines which classes and methods can actually be reached at runtime and need to be included in the native binary. Modular applications produce smaller, more predictable native images. For teams building serverless functions or CLI tools in Java, this matters considerably.
6.4 Library authors who care about API hygiene
For open source library developers who want to enforce a clean public API — where internal implementation details are genuinely inaccessible rather than just conventionally discouraged — JPMS works well. The exports directive is a real enforcement mechanism, not a convention or a comment. A growing number of modern Java frameworks targeting Java 11 and above have adopted explicit module descriptors precisely for this reason.
7. The Adoption Landscape in 2026
JPMS Adoption by Dependency Layer (2025 estimate)

Where JPMS Sees Real Use vs. Where It Doesn’t

| Context | JPMS status | Why |
|---|---|---|
| JDK itself | ✓ Fully modular since Java 9 | Oracle modularised the platform; all users benefit silently |
| Spring Boot apps | ✗ Not adopted (Spring 6.x) | Spring’s reflection model; team deprioritised in favour of GraalVM |
| Micronaut / Quarkus | Partial — improving | AOT-friendly design; still working through dependency graph |
| CLI tools & desktop apps | ✓ Strong use case for jlink / jpackage | Container size and self-contained distribution matter here |
| Serverless (GraalVM native) | ✓ Growing adoption | Modular input improves native image quality and size |
| OSGi-based enterprise apps | Coexists but separate | OSGi teams have their own runtime modularity model |
| Major open-source libraries | ✓ Broad adoption of explicit modules or Automatic-Module-Name | Ecosystem pressure; major libraries have largely made the move |
| Mid-tier / legacy libraries | ✗ Many still non-modular | Maintenance burden; no strong migration pressure |
The migration timeline (realistic)
A 2025 analysis from Java Code Geeks outlines a plausible roadmap: 2023–2025 sees progressive conversion of major libraries to explicit modules; 2025–2030 sees most active projects fully modularised; post-2030, automatic modules become rare and are used only for legacy dependencies. We are currently in the middle phase. The train is moving — it is simply moving slowly.
8. What We Have Learned
JPMS is a genuinely well-designed system that solved a real problem and arrived too late to the party it was supposed to host. By 2017, the ecosystem had adapted around the classpath. Maven and Gradle had matured. Microservices had changed how teams thought about isolation — you isolate at the JVM process level, not within a single JVM. OSGi served the niche that needed real runtime modularity. And Spring, the dominant framework, was architecturally incompatible with JPMS’s core premise.
None of that makes JPMS irrelevant. It successfully modularised the JDK itself, eliminating access to internal APIs that were a recurring source of security vulnerabilities and cross-version pain. Its toolchain — particularly jlink and its integration with GraalVM — delivers real, measurable benefits for container-based and native deployments. And its gradual ecosystem adoption, however slow, is headed in the right direction.
The honest answer to “should I use JPMS in my application?” in 2026 is: it depends on your deployment model. Building a Spring Boot monolith? You probably won’t need to write a module-info.java, and the framework’s GraalVM path may give you the runtime efficiency you’re after anyway. Building a CLI tool, a library with a clean public API, or a serverless function with a cold-start budget? JPMS and jlink are worth the investment. What JPMS was not, and never was, is a drop-in solution to classpath problems for existing large applications. Understanding that distinction is what separates an honest assessment from yet another tutorial.
9. Further Reading
- JPMS vs OSGi deep dive — InfoQ
- JPMS negative benefits for library authors — Stephen Colebourne
- The Future of Java 2026 — Java Code Geeks

