One of the most common misconceptions when working with the Java Platform Module System (JPMS) is assuming that everything remains as open as it was on the classpath. Developers often face confusing errors like:
Error: package com.example.internal is not visible
This happens because JPMS enforces strong encapsulation—only explicitly exported packages are accessible outside the module. While this may initially feel restrictive, strong encapsulation is one of the biggest strengths of JPMS. It prevents accidental dependencies, improves maintainability, and enhances security for large enterprise systems and microservices.
In this tutorial, we’ll explore what strong encapsulation means, how it works in practice, and how to apply it effectively in your projects.
What is Strong Encapsulation?
In JPMS, strong encapsulation means:
- Only packages listed in
exports
are visible outside the module. - Non-exported packages remain completely hidden from other modules.
- Reflection is restricted unless explicitly opened with
opens
.
This gives developers precise control over what is public API and what remains internal.
Example of Strong Encapsulation
Module Descriptor
module com.example.payment {
exports com.example.payment.api; // API visible to other modules
// com.example.payment.internal remains hidden
}
Code Structure
com.example.payment/
├── module-info.java
├── com/example/payment/api/PaymentService.java
└── com/example/payment/internal/PaymentValidator.java
PaymentService
is exported and visible to other modules.PaymentValidator
is internal and inaccessible outsidecom.example.payment
.
Trying to use PaymentValidator
from another module will cause a compile-time error.
Benefits of Strong Encapsulation
- Cleaner APIs → Only stable contracts are exposed.
- Security → Internal classes are protected from external misuse.
- Maintainability → Internal implementations can change freely without breaking consumers.
- Reduced Coupling → Modules depend only on declared APIs.
Reflection and Encapsulation
Frameworks often use reflection (Spring, Hibernate, Jackson). Strong encapsulation blocks reflective access unless explicitly opened.
Example
module com.example.user {
exports com.example.user.api;
opens com.example.user.entity to hibernate.core;
}
com.example.user.api
is exported as an API.com.example.user.entity
is opened only for Hibernate reflection.
Avoid using open module
in production as it makes all packages reflective, defeating encapsulation.
Pitfalls & Misuse Cases
- Exporting all packages → Removes encapsulation benefits.
- Using
open module
unnecessarily → Creates security holes. - Confusing exports with opens →
exports
is for API access,opens
is for reflection. - Over-modularization → Splitting too much may complicate encapsulation rules.
Best Practices for Strong Encapsulation
- ✅ Export only stable API packages.
- ✅ Keep internal logic in non-exported packages.
- ✅ Use
opens
selectively for reflection-heavy frameworks. - ✅ Never use
open module
unless absolutely necessary (debugging, testing). - ✅ Organize packages into clear API vs internal boundaries.
- ✅ Regularly review exports to avoid leaking internals.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → Strong encapsulation introduced with JPMS (
exports
,opens
) - Java 11 → Tooling improvements for modular applications
- Java 17 → Security refinements and performance improvements
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of strong encapsulation as a corporate office structure:
- Reception and meeting rooms (API packages) are open to visitors.
- Internal workspaces (internal packages) are accessible only to employees.
- Some sensitive areas (databases, archives) may be opened only to auditors (frameworks via
opens
). - This ensures both security and organization.
Summary + Key Takeaways
- Strong encapsulation hides internals and exposes only APIs via
exports
. - Reflection requires explicit
opens
. - Avoid
open module
; prefer fine-grainedopens
. - Encapsulation improves security, maintainability, and modular integrity.
- Best practice: Treat JPMS as a tool to separate what’s stable API from what’s implementation detail.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit exports.
Q2. Why do I get “package is not visible” errors?
Because the package isn’t exported in module-info.java
.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies so downstream modules don’t need to declare them.
Q4. How do open
and opens
differ?
open module
→ all packages are reflective.opens
→ only specific packages are reflective.
Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Good for migration but not for production.
Q6. How does JPMS improve security compared to the classpath?
By hiding internals, blocking reflective access, and preventing split packages.
Q7. When should I use jlink
vs jmod
?
jlink
→ Build custom runtime images.jmod
→ Package and distribute modules.
Q8. Can I migrate legacy projects incrementally?
Yes. Start with automatic modules, then add explicit module-info.java
.
Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or keep them on the classpath temporarily.
Q10. Do frameworks like Spring and Hibernate fully support strong encapsulation?
Not entirely—most require selective opens
to function.