Key Takeaways
- Most teams skip JPMS entirely — and that might be the smartest call for mid-size codebases under 500k lines.
- Split packages are the silent killer — they crash at build time in ways that look like runtime errors, and debugging them wastes days.
- Automatic module names are technical debt — every JAR your project pulls in without a module descriptor creates a hidden maintenance burden.
Here's what nobody tells you about the Java Module System (JPMS): it's not a silver bullet for modularization, and it's not ready for most mid-size codebases. After spending eight months migrating a 400k-line monolith into modules, I learned that the hardest part isn't the module descriptor. It's undoing the structural anti-patterns your team built while working in the dark.
We thought JPMS would impose order. It did, but not the kind anyone promised.
The Split Package Trap Nobody Warns About
Split packages happen when two different JARs contain classes in the same Java package. Classically, Java let this slide. The classloader merged them silently. Your code compiled. Your tests passed. And then JPMS arrived and said: sorry, that's a hard error now.
The problem isn't that JPMS catches this. The problem is that the error message is useless. You'll see something like “module A opens package com.example.internal, but module B also provides it.” Great. Now you're playing detective across five dependency layers to find which transitive dependency is hiding a competing implementation.
In our migration, three of our five biggest blockers were split packages. The worst offender? A logging utility we thought we owned. It turned out our ORM library bundled a forked version in a subtly different package name that leaked through a transitive dependency. Two days of grief for a bug that didn't even affect runtime behavior before JPMS.
The fix: Use jdeps --check early and often. Run it against your entire dependency graph before you write a single module-info.java. It will surface hidden package collisions that would otherwise ambush you at build time.
Automatic Module Names Are Debt You Can't See
When a JAR lacks a module-info.class, the JVM generates an automatic module name from the filename. spring-core-6.1.4.jar becomes spring.core. commons-lang3-3.14.0.jar becomes org.apache.commons.lang3.
These names aren't guaranteed to be stable. The JDK spec explicitly says automatic module names may change between releases. When they do, your requires statements break. Suddenly your CI pipeline fails for a reason that has nothing to do with your code.
We discovered this when upgrading from Java 21 to Java 23. Two transitive dependencies shifted their automatic names, and the error messages pointed us in completely wrong directions. The real issue was invisible to anyone who didn't read the fine print.
What to do instead: Create explicit module descriptors for your critical JARs. Use the jar --describe-module command to audit what names your dependencies are getting. For libraries you control, always ship a proper module-info.java. For third-party JARs without descriptors, consider using the --patch-module flag during build to layer a descriptor on top.
When to Skip JPMS Entirely
Here's the contrarian take: most mid-size Java projects shouldn't use JPMS at all.
If your codebase is under 500,000 lines and your team is under 15 developers, the module system probably isn't buying you anything. It adds build complexity, slows down IDE performance, and introduces a new layer of debugging overhead. The separation benefits it promises only materialize when you have genuinely independent deployable units.
JPMS shines when you're building libraries or platforms. It matters for JVM-based frameworks, cloud-native runtimes, and projects where you need compile-time guarantees about internal API boundaries. For a typical enterprise application with a monolithic deployment, multi-module Gradle or Maven is the right level of granularity.
Our team made the mistake of treating JPMS as a mandatory modernization checkbox. We should have treated it as a strategic decision. The module system would have added value if we were shipping a product used by external developers. Since we're shipping an internal application, the effort-to-reward ratio was terrible.
Gradle vs. Maven: The Module Configuration War
If you do decide to modularize, the build tool choice matters more than you'd expect. Maven's multi-module setup is mature but rigid. You define module relationships in pom.xml files, and those relationships are enforced by the reactor build. It works, but it's slow for incremental builds and unforgiving when you introduce circular dependencies.
Gradle's Kotlin DSL gives you more expressiveness. You can define module interfaces as pure Kotlin interfaces, use shared configuration blocks, and get better build caching. The downside? Gradle's module system integration is still catching up to Maven's. You'll find more community examples for Maven, but Gradle wins on developer experience for complex dependency graphs.
For our migration, we used Gradle with a hybrid approach. We kept the build modules separate from the runtime modules. This meant our settings.gradle defined the build structure, while module-info.java files defined the runtime visibility. It added a layer of indirection but prevented our build modules from leaking implementation details into the runtime contract.
The Real Cost: IDE and Build Fragmentation
Before you commit to JPMS, talk to your developers about their daily workflow. The module system changes how IDEs compile, debug, and navigate code. IntelliJ IDEA handles JPMS reasonably well now, but Eclipse and VS Code fall behind. If your team uses multiple IDEs, the friction is real.
Build times also increase. The module compiler has more work to do, and incremental builds don't always invalidate the right classes when you change a package-private API. We saw a 15% increase in our CI build times after going fully modular. For a 20-minute build, that's three minutes of lost productivity per deployment.
These costs are invisible until you're already in the migration. Plan for them explicitly. Track build time metrics before and after. Survey your team's IDE experience weekly during the transition. The data will tell you whether JPMS is worth the investment.
Conclusion: Be Honest About What Modularization Buys You
JPMS is a powerful tool. It's also not the answer to every modularization problem. If you're working on a large-scale platform or library, the compile-time guarantees are worth the pain. If you're running a mid-size application with a small team, multi-module Gradle or Maven will get you 90% of the benefit with 10% of the complexity.
The mistake isn't using JPMS. The mistake is using it because it's fashionable or because leadership thinks “modular” equals “better.” Choose the right tool for your actual constraints, not the one that looks good on a whiteboard.
Still thinking about modularizing your Java project? Drop your questions in the comments. We've all been through the migration scars, and the lessons are worth sharing.
