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
ordermodule depends on theusermodule. - Only exported packages of
com.example.userare 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.apiis available externally. - Internal packages like
com.example.user.internalstay 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;
}
orderrequiresuserto access user APIs.paymentrequiresorderand re-exports it withrequires transitive.- Any module that depends on
paymentautomatically 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 transitivesparingly 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
requiresandexports - 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
requiresdeclares dependencies between modules.exportscontrols what packages are visible externally.- Variants like
requires transitiveand selectiveexportsprovide 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 moduleopens everything.opensopens 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?
jlinkbuilds custom runtime images.jmodpackages 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.