Many developers run into frustrating errors like “package not found” or “package is not visible” when moving from the classpath to the module path. What worked seamlessly in legacy Java suddenly breaks, leaving teams scrambling to understand why their code can no longer access certain classes.
This pain often arises from not fully grasping three foundational concepts: the module, the module descriptor (module-info.java), and the module path. These form the backbone of the Java Platform Module System (JPMS). Without mastering them, modularizing enterprise systems, microservices, or even simple libraries becomes error-prone.
Understanding these concepts matters in real-world scenarios:
- Enterprise apps need clear boundaries between services.
- Cloud-native deployments benefit from minimized runtimes with only necessary modules.
- Legacy migration projects must balance classpath compatibility with modern modularization.
What is a Module?
A module is a named collection of code and resources (packages, classes, native libraries, configuration files) with clear boundaries. Unlike JARs on the classpath, modules explicitly state:
- What they depend on (
requires
). - What they expose to others (
exports
).
Think of a module as an office department: it decides what internal documents remain private and what reports it shares with other departments.
Example
// File: module-info.java
module com.example.user {
exports com.example.user.api;
}
Here:
- The
com.example.user
module exists. - Only the
user.api
package is visible outside; internal packages remain hidden.
The Module Descriptor (module-info.java)
The module descriptor is a special file placed in the root of a module, named module-info.java
. It describes:
- Dependencies →
requires
other modules. - Exports → Packages available for other modules.
- Services → Declared using
uses
andprovides
. - Openness → Reflection access with
open
oropens
.
Example
module com.example.payment {
requires com.example.user;
exports com.example.payment.api;
uses com.example.payment.spi.PaymentProcessor;
provides com.example.payment.spi.PaymentProcessor
with com.example.payment.impl.DefaultProcessor;
}
Explanation:
requires
→ Depends on theuser
module.exports
→ Makes thepayment.api
package available externally.uses
→ Declares a service interface.provides ... with ...
→ Supplies an implementation.
Without module-info.java
, a JAR is treated as an unnamed module (essentially classpath behavior).
The Module Path vs Classpath
The module path replaces the traditional classpath for modular applications.
Comparison
Feature | Classpath | Module Path |
---|---|---|
Scope | All classes globally visible | Controlled visibility via exports |
Dependency Handling | No explicit dependencies | Explicit requires declaration |
Error Detection | Runtime (late) | Compile-time (early, safer) |
Encapsulation | Weak, accidental access possible | Strong, hides internals |
Deployment | Fat JARs | Custom runtime via jlink , jmod |
Example Run
# Using classpath
java -cp user.jar:payment.jar com.example.Main
# Using module path
java --module-path mods -m com.example.payment/com.example.Main
On the module path, only what’s explicitly exported is visible.
Pitfalls and Misuse Cases
- Classpath/Module Path Confusion – Mixing them incorrectly leads to “class not found” errors.
- Overusing Automatic Modules – JARs without descriptors become automatic modules but may break builds.
- Open Everything – Declaring
open module
bypasses encapsulation and defeats JPMS’s purpose. - Migration Pain – Non-modular third-party libraries complicate adoption.
Best Practices
- Always create clear
module-info.java
files for your own code. - Avoid over-modularizing (too many small modules add complexity).
- Use
requires transitive
carefully to re-export stable dependencies. - Keep reflection access (
opens
) limited to frameworks that truly need it. - Use
jlink
to create lean runtimes for Docker/Kubernetes.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced: modules,
module-info.java
, modular JDK - Java 11 → Tooling refinements for
javac
,jlink
- Java 17 → Performance and security improvements for JPMS
- Java 21 → No significant updates across Java versions for this feature
Real-World Example: Modularizing a Legacy App
Imagine a legacy monolith with user
, order
, and payment
packages. Using modules:
// user module
module com.example.user {
exports com.example.user.api;
}
// order module
module com.example.order {
requires com.example.user;
exports com.example.order.api;
}
// payment module
module com.example.payment {
requires transitive com.example.order;
exports com.example.payment.api;
}
- Clear dependency flow: payment → order → user.
- Encapsulation: Internal classes hidden, only APIs visible.
- Incremental migration: Teams can modularize piece by piece.
Summary + Key Takeaways
- A module is a named boundary around code/resources.
- The module descriptor (
module-info.java
) defines dependencies, exports, and services. - The module path enforces strong encapsulation compared to the classpath.
- Avoid pitfalls like automatic modules and overuse of open modules.
- JPMS brings clarity, maintainability, and security for modern Java applications.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath makes everything visible globally, while module path enforces explicit dependencies and access control.
Q2. Why do I get “package is not visible” errors when using modules?
Because only explicitly exported packages in module-info.java
are accessible to other modules.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies so downstream modules don’t need to declare them separately.
Q4. How do open
and opens
differ in reflection?
open module
→ All packages open at runtime.opens
→ Only specific packages open for reflection.
Q5. What are automatic modules, and should I use them?
JARs without descriptors automatically treated as modules. Use only for temporary migration, not long-term.
Q6. How does JPMS improve security compared to the classpath?
By hiding non-exported internals, reducing the attack surface and misuse risk.
Q7. When should I use jlink
vs jmod
?
jlink
→ Build custom runtime images.jmod
→ Package modules for distribution.
Q8. Can I migrate a legacy project to modules incrementally?
Yes, start by modularizing a single JAR, then progressively modularize others.
Q9. How do I handle third-party libraries that are not modularized?
Use them on the classpath or as automatic modules until they become modularized.
Q10. Do Java frameworks (Spring, Hibernate) fully support modules?
Not fully. Many rely on reflection, requiring opens
for runtime access.