Many developers hit a wall when transitioning to JPMS because they assume it works exactly like the classpath. They’re often puzzled by errors like:
Error: module not found
Error: package is not visible
This happens because JPMS has a completely different internal resolution model compared to the classpath. Instead of a flat world where everything can see everything, JPMS introduces a graph of modules, dependencies, and encapsulation rules.
Understanding JPMS internals is crucial for building enterprise systems, modularizing legacy apps, and creating custom runtime images with tools like jlink.
Core JPMS Internals
At its heart, JPMS introduces several concepts that govern how code is compiled and executed.
1. Module Descriptors
Defined in module-info.java, they declare dependencies and exports.
module com.example.user {
exports com.example.user.api;
requires com.example.order;
}
This tells the JPMS resolver:
- The module is named
com.example.user. - It exports
com.example.user.api. - It requires another module
com.example.order.
2. Module Resolution Graph
Unlike the classpath (flat, global visibility), JPMS builds a directed graph of modules at startup.
- Nodes → modules.
- Edges → dependencies (
requires).
If a dependency isn’t found, the graph build fails and the JVM won’t start the application.
3. Layers and Class Loaders
Modules are loaded in layers.
- Boot Layer → standard JDK modules (
java.base,java.sql, etc.). - Application Layer → user-defined modules.
Each layer has its own classloader, ensuring strict isolation.
4. Strong Encapsulation
By default, only packages explicitly exported in module-info.java are visible. Internals remain hidden. This solves the problem of accidental dependencies in large systems.
module com.example.payment {
exports com.example.payment.api; // public
// com.example.payment.internal is hidden
}
5. Reflection and Openness
Frameworks that rely on reflection (Spring, Hibernate) often fail unless packages are opened.
module com.example.payment {
opens com.example.payment.entity to hibernate.core;
}
exports→ compile-time + runtime visibility.opens→ runtime reflective visibility.
6. Services with ServiceLoader
JPMS integrates with the ServiceLoader mechanism for dynamic discovery.
module com.example.email {
provides com.example.notification.api.Notifier
with com.example.email.EmailNotifier;
}
module com.example.app {
uses com.example.notification.api.Notifier;
}
Pitfalls in JPMS Internals
- Classpath fallback → Mixing classpath and module path leads to confusing errors.
- Split packages → Not allowed across modules.
- Overusing open modules → Breaks encapsulation.
- Transitive chaos → Overusing
requires transitivecreates unwanted dependencies. - Non-modularized libraries → Still a challenge; need automatic or unnamed modules.
Best Practices for JPMS Internals
- Always use explicit requires/exports.
- Keep APIs in dedicated packages separate from internals.
- Avoid
open modulein production; useopenssparingly. - Use
jdepsto analyze dependencies and modular boundaries. - Prefer services (
provides/uses) over hard dependencies for extensibility.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced: module descriptors, strong encapsulation, service loader integration
- Java 11 → Refinements and tooling improvements (
jdeps, IDE support) - Java 17 → Performance and security refinements for modular systems
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of JPMS as a city with neighborhoods (modules):
- Each neighborhood has gates (exports) for outsiders.
- Some gates allow only auditors (frameworks) in (
opens). - Roads (requires) connect neighborhoods.
- A central city planner (resolver) checks if all roads and gates are valid before the city opens.
This prevents random construction (classpath chaos) and ensures order.
Summary + Key Takeaways
- JPMS builds a module graph at startup.
- Modules expose only what they declare, keeping internals hidden.
- Reflection requires
opens. - Services enable loose coupling with
providesanduses. - Best practice: keep APIs clean, internals hidden, and avoid unnecessary openness.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit boundaries.
Q2. Why do I get “package is not visible” errors in JPMS?
Because the package wasn’t exported in the module descriptor.
Q3. What is the purpose of requires transitive?
It re-exports dependencies so downstream modules automatically inherit them.
Q4. How do open and opens differ?
open module→ all packages reflective.opens→ only specific packages reflective.
Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Use during migration, but not long-term.
Q6. How does JPMS improve security compared to classpath?
By hiding internals, enforcing strict resolution, and limiting reflection.
Q7. When should I use jlink vs jmod?
jlink→ Create custom runtime images.jmod→ Package and distribute modules.
Q8. Can I modularize incrementally?
Yes, use automatic and unnamed modules as stepping stones.
Q9. How do I handle third-party libraries that aren’t modularized?
Use as automatic modules or on the classpath temporarily.
Q10. Do frameworks like Spring and Hibernate fully support JPMS?
Partial support; many need opens for reflection.