Many developers adopting the Java Platform Module System (JPMS) assume that simply adding a module-info.java
file instantly makes their project modular. The reality is far more complex. Common mistakes—such as mixing classpath and module path, exporting everything, or abusing opens
—often lead to broken encapsulation, runtime errors, or even security vulnerabilities.
In large enterprise systems or microservice architectures, these pitfalls can accumulate into costly technical debt. Understanding the most frequent anti-patterns in JPMS usage helps ensure that your modular applications remain secure, maintainable, and future-proof.
Think of JPMS as designing departments in a company. If every department shares all its documents without restrictions, chaos ensues. Proper boundaries must be enforced, and pitfalls avoided.
Pitfall 1: Mixing Classpath and Module Path
Problem
Running part of the app on the classpath and part on the module path leads to inconsistent visibility.
Example:
java --module-path mods -cp lib/* -m com.example.app/com.example.Main
Why It’s Bad
- Undermines modular boundaries
- Causes unpredictable runtime behavior
- Hard to debug dependency issues
Solution
Migrate incrementally but consistently—prefer the module path for all modularized code.
Pitfall 2: Exporting Everything
Problem
Developers often export all packages for convenience:
module com.example {
exports com.example.core;
exports com.example.internal; // should not be public
}
Why It’s Bad
- Breaks encapsulation
- Exposes internal APIs unnecessarily
- Makes refactoring dangerous
Solution
Export only public APIs. Keep implementation details hidden.
Pitfall 3: Overusing open
or opens
Problem
Declaring entire modules as open
:
open module com.example {
requires java.sql;
}
Why It’s Bad
- Defeats JPMS encapsulation
- Gives reflective frameworks unrestricted access
- Creates long-term security risks
Solution
Use opens
selectively for specific packages that require reflection.
Pitfall 4: Overreliance on Automatic Modules
Problem
Placing non-modular JARs on the module path creates automatic modules.
Example:
legacy-lib.jar → automatic module: legacy.lib
Why It’s Bad
- Module names are unstable (derived from filenames)
- Exports all packages by default → weak encapsulation
- Breaks when library updates change JAR naming
Solution
Prefer libraries with module-info.java
or add Automatic-Module-Name
in the manifest.
Pitfall 5: Misusing requires transitive
Problem
Adding requires transitive
everywhere:
module com.example.service {
requires transitive com.example.core;
}
Why It’s Bad
- Pollutes dependency graphs
- Creates unintended coupling between modules
- Makes maintenance harder
Solution
Use transitive
sparingly, only when an API genuinely depends on another API.
Pitfall 6: Ignoring Reflection in Frameworks
Problem
Hibernate, Spring, and similar frameworks fail with:
IllegalAccessException: module does not open package ...
Why It’s Bad
- Blocks frameworks relying on reflection
- Leads to runtime crashes
Solution
Use opens
in module-info.java
or runtime flags:
--add-opens com.example.entities/com.example.entities=org.hibernate.orm.core
Pitfall 7: Overcomplicating Module Structures
Problem
Creating too many small modules:
com.example.util.string
com.example.util.math
com.example.util.io
Why It’s Bad
- Leads to dependency sprawl
- Adds overhead to builds and runtime
- Harder to manage for large teams
Solution
Balance granularity—modules should represent meaningful boundaries, not individual utilities.
What's New in Java Versions?
- Java 5–8 → N/A (no JPMS)
- Java 9 → JPMS introduced, modules became part of the JDK
- Java 11 → Tooling improvements (
jdeps
, better support for migration) - Java 17 → Security and performance refinements for JPMS
- Java 21 → No significant updates across Java versions for this feature
Real-World Analogy
Using JPMS incorrectly is like leaving all office doors unlocked and giving everyone master keys. While convenient, it leads to confusion, misuse, and potential breaches. Correct usage enforces the right level of access control for maintainability and security.
Summary & Key Takeaways
- Avoid mixing classpath and module path
- Export only public APIs, not internals
- Use
opens
selectively, not globally - Don’t rely heavily on automatic modules
- Use
requires transitive
sparingly - Balance module granularity
- Handle reflection-based frameworks explicitly
- Proper modularization leads to secure, maintainable systems
FAQ: Java Module Pitfalls
1. What is the difference between classpath and module path?
Classpath loads everything without boundaries; module path enforces explicit dependencies.
2. Why do I get “package is not visible” errors?
Because you didn’t export or open the package correctly.
3. What is the purpose of requires transitive
?
It passes dependencies along automatically, but should be used sparingly.
4. How do open
and opens
differ?open
applies to the entire module, while opens
applies to specific packages.
5. What are automatic modules, and should I use them?
Temporary tools for non-modular JARs. Avoid in production.
6. How does JPMS improve security over classpath?
By restricting access to non-exported packages and APIs.
7. When should I use jlink vs jmod?
Use jlink
for runtime images, jmod
for packaging modules.
8. Can I migrate a legacy project incrementally?
Yes, start with core APIs and gradually modularize.
9. How do I handle third-party libraries that aren’t modularized?
Use Automatic-Module-Name
or modularize with tools like moditect
.
10. Do frameworks like Spring and Hibernate fully support JPMS?
Yes, but they often require opens
for reflection-heavy features.