A common frustration developers face when adopting Java Modules is the “package is not visible” error. The culprit? Incorrect use of requires
and exports
in module-info.java
. On the classpath, everything is globally accessible, but on the module path, visibility is tightly controlled.
This matters because in enterprise applications, microservices, and modularized systems, you want strong boundaries: APIs should be accessible, but internals must remain hidden. By mastering requires
and exports
, you can avoid hidden coupling, improve security, and build maintainable codebases that scale to large projects.
The Role of requires
in Java Modules
The requires
directive specifies dependencies between modules. Without it, your code won’t compile when trying to use classes from another module.
Example
// order module descriptor
module com.example.order {
requires com.example.user;
}
- The
order
module depends on theuser
module. - Only exported packages of
com.example.user
are visible.
Variants of Requires
requires
→ Regular dependency.requires transitive
→ Makes the dependency available to downstream modules.
module com.example.payment {
requires transitive com.example.order;
}
Now, any module that requires payment
also implicitly gets access to order
.
requires static
→ Dependency needed only at compile-time, optional at runtime. Useful for annotation processors.
The Role of exports
in Java Modules
The exports
directive controls which packages a module makes available to others.
Example
// user module descriptor
module com.example.user {
exports com.example.user.api;
}
- Only
com.example.user.api
is available externally. - Internal packages like
com.example.user.internal
stay hidden.
This ensures encapsulation and prevents misuse of internal classes.
Selective Exports
You can export a package to a specific module:
module com.example.user {
exports com.example.user.internal to com.example.admin;
}
Now, only com.example.admin
can access this internal package.
Real-World Example
Consider a shopping system:
// 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;
}
order
requiresuser
to access user APIs.payment
requiresorder
and re-exports it withrequires transitive
.- Any module that depends on
payment
automatically gets access toorder
.
Pitfalls and Misuse Cases
- Forgetting exports → Causes “package not visible” errors.
- Exporting everything → Defeats encapsulation and resembles classpath behavior.
- Overusing requires transitive → Leads to hidden dependencies and tight coupling.
- Relying on automatic modules → Works temporarily, but fragile in large projects.
Best Practices
- Export only public API packages; keep internals private.
- Use
requires transitive
sparingly and only for stable APIs. - Prefer explicit dependencies instead of relying on automatic modules.
- Document module boundaries clearly for maintainability.
- Combine JPMS with build tools (Maven/Gradle) for multi-module projects.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → Introduction of JPMS with
requires
andexports
- Java 11 → Improved tooling support for modular compilation and packaging
- Java 17 → Performance/security improvements in JPMS runtime checks
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of modules as departments in a company:
exports
→ The department shares official reports with others.requires
→ A department depends on reports from another.requires transitive
→ If Finance relies on HR policies, and Payroll relies on Finance, then Payroll automatically gets HR policies too.
This analogy illustrates how controlled sharing prevents chaos while enabling structured collaboration.
Summary + Key Takeaways
requires
declares dependencies between modules.exports
controls what packages are visible externally.- Variants like
requires transitive
and selectiveexports
provide fine-grained control. - Avoid pitfalls like overusing transitive dependencies or exporting everything.
- JPMS enforces clarity, encapsulation, and security in modular applications.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit exports and requires.
Q2. Why do I get “package is not visible” errors when using modules?
Because the package wasn’t exported in module-info.java
.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies, making them available to downstream modules.
Q4. How do open
and opens
differ in reflection?
open module
opens everything.opens
opens specific packages for reflection.
Q5. What are automatic modules, and should I use them?
They’re JARs without descriptors treated as modules. Use them only for temporary migration.
Q6. How does JPMS improve security compared to the classpath?
By hiding internal classes and limiting what is exported or opened.
Q7. When should I use jlink
vs jmod
?
jlink
builds custom runtime images.jmod
packages modules for distribution.
Q8. Can I migrate a legacy project to modules incrementally?
Yes, start with one JAR/module and expand gradually.
Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or keep them on the classpath.
Q10. Do frameworks like Spring/Hibernate fully support modules?
Not fully; they often require opens
to enable reflective access.