Java Modules
The Java Platform Module System (JPMS), introduced in Java 9, groups packages into modules with explicit declarations of what they require and expose, enforcing strong encapsulation the classpath never could.
Before Java 9, every public class on the classpath was accessible to every other class on the classpath — there was no language-level notion of 'this package is internal, don't touch it.' The Java Platform Module System (JPMS), delivered in Java 9 (Project Jigsaw), introduces the module as a new unit of encapsulation above the package: a named group of packages with an explicit descriptor, module-info.java, stating exactly what it requires from other modules and exactly what it exports for others to use. Packages not exported are strongly encapsulated — inaccessible from outside the module even via reflection, unless the module explicitly opens them.
Why JPMS Exists
Two problems motivated JPMS. First, weak encapsulation: any public class, in any package, on the classpath was reachable from anywhere else on the classpath — internal implementation packages had no real protection, only convention (naming them 'internal' or 'impl' and hoping). Second, the JDK itself had grown a large, monolithic rt.jar containing every core class, whether an application needed it or not, with no way to build a smaller custom runtime. JPMS addresses both: it lets any codebase (including the JDK itself, now split into dozens of modules like java.base, java.sql, java.xml) declare real, enforced boundaries.
module-info.java
A module is declared by a single file named module-info.java at the root of its source tree, compiled into module-info.class. It names the module and lists its directives.
This declares module com.example.billing, which needs java.sql and re-exposes com.example.core to its own consumers (requires transitive), publicly exports its api package, exports dto only to one named consumer module, opens entity for reflective access by Hibernate specifically, and both consumes and provides implementations of a service interface.
module com.example.billing {
requires java.sql;
requires transitive com.example.core;
exports com.example.billing.api;
exports com.example.billing.dto to com.example.billing.client;
opens com.example.billing.entity to org.hibernate.orm.core;
uses com.example.billing.spi.PaymentProvider;
provides com.example.billing.spi.PaymentProvider
with com.example.billing.internal.DefaultPaymentProvider;
}requires: Declaring Dependencies
requires <module> declares a compile-time and run-time dependency on another module; without it, the compiler rejects any reference to that module's exported types, even if the module is physically present on the module path. requires static declares a compile-time-only (optional at run time) dependency, useful for annotations or tooling not needed once compiled. requires transitive re-exposes the dependency to any module that in turn requires this one — without transitive, each consumer must independently declare its own requires for every module whose types appear in your public API, or their code won't compile against it.
exports: Controlling Compile/Run-Time Visibility
exports <package> makes every public type in that package accessible (for normal compile-time linking and method calls) to any module that requires this one. A package not listed in any exports directive is completely inaccessible from outside the module — even its public classes cannot be referenced, imported, or compiled against by other modules. exports <package> to <module1, module2> restricts that visibility to specifically named modules only, a qualified export, useful for internal APIs shared with one trusted consumer without opening them to everyone.
com.example.billing/
├── module-info.java (declares: exports com.example.billing.api;)
├── com/example/billing/api/ <- exported: visible to all requiring modules
│ └── Invoice.java
└── com/example/billing/internal/ <- NOT exported: invisible outside the module
└── TaxCalculator.javaopens: Reflective Access
exports governs ordinary compile-time/run-time access (calling methods, referencing types normally). opens governs deep reflective access — setAccessible(true) on private/non-public members, used heavily by serialization libraries, dependency-injection frameworks, and ORMs. A package can be opened without being exported (reflection-only access, e.g. for a framework that needs to construct and populate entity objects but shouldn't be part of the public compile-time API), opened to specific modules only (opens ... to), or the whole module can be declared open module, which opens every package in it for reflection while exports rules still govern normal access.
open module com.example.entities {
exports com.example.entities.api;
// every package, including unexported ones, is reflectively
// accessible at run time — commonly used for JPA entity modules
}uses / provides: The Service-Loader Mechanism
JPMS formalizes the classic java.util.ServiceLoader pattern. uses <service-interface> declares that a module consumes implementations of a service located via ServiceLoader.load(). provides <service-interface> with <impl-class> declares that a module supplies a specific implementation, replacing the old META-INF/services text-file registration with a compiler-checked module declaration (though the META-INF/services mechanism still works for unnamed/automatic modules).
Strong Encapsulation
This is JPMS's central guarantee: a non-exported, non-opened package's classes cannot be accessed from outside the module — not via normal compilation, and not via reflection either, even with setAccessible(true), which now throws InaccessibleObjectException against strongly encapsulated members. This closes a gap the classpath never had: code that relied on reflectively poking into java.lang internals or other libraries' 'private' packages breaks under the module path unless those modules explicitly open the relevant packages (the JDK itself opens very little of its internals by default, which is precisely why some legacy reflection-heavy tools needed updates when moving to JPMS).
Named vs. Unnamed Modules
Code compiled with a module-info.java belongs to a named module. Code placed on the traditional classpath (no module-info.java at all) is loaded into the unnamed module — a catch-all module that reads every other module and exports all its own packages to everyone, effectively preserving pre-Java-9 classpath semantics for code that hasn't adopted JPMS. A module path and the classpath can coexist in the same application; named modules and the unnamed module can generally see each other, though a named module cannot requires the unnamed module by name (there isn't one) — it can only depend on it implicitly by being launched with mixed classpath/module-path configuration.
Automatic Modules
A plain JAR (no module-info.class) placed on the module path — rather than the classpath — becomes an automatic module. The JVM derives its module name automatically (from the JAR's Automatic-Module-Name manifest entry if present, otherwise guessed from the JAR filename), and it automatically requires every other module and exports all its own packages, acting as a bridge for pre-modular libraries that haven't yet published a real module-info. This lets existing JARs participate in a modular application without modification, at the cost of not enforcing real encapsulation themselves.
Relying on a filename-derived automatic module name is fragile — renaming the JAR changes the module name. Library maintainers are encouraged to add an explicit Automatic-Module-Name entry to MANIFEST.MF as a stable stepping stone before shipping a full module-info.java.
Module Path vs. Classpath
Compiling and running modular code uses --module-path (or -p) instead of, or alongside, -classpath. javac --module-path mods -d out --module com.example.billing compiles a module by name; java --module-path mods -m com.example.billing/com.example.billing.Main launches it, naming both the module and its main class.
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out -m com.example.billing/com.example.billing.MainPractical Guidance
Many everyday applications, especially those already invested in a build tool and dependency set built around the classpath, still run comfortably unmodularized on the classpath, on the unnamed module, and JPMS's classpath-compatibility design deliberately allows that. Adopting JPMS pays off most for library authors who want to enforce real API boundaries, and for applications building custom minimal runtime images (via jlink), which requires the whole dependency graph to be modular.
Key Takeaways
JPMS (Java 9+) adds the module as a unit of encapsulation above the package, declared in module-info.java.
requires declares dependencies (requires transitive re-exposes them); exports controls ordinary compile/run-time visibility; opens controls deep reflective access, independently of exports.
Non-exported, non-opened packages are strongly encapsulated — inaccessible even via reflection with setAccessible(true).
Classpath code lives in the unnamed module, preserving pre-JPMS behavior; plain JARs placed on the module path become automatic modules as a migration bridge.
uses/provides formalize the ServiceLoader pattern with compiler-checked declarations.
Assuming exports also grants reflective access to private members.
Use opens (or opens ... to) explicitly if a package needs deep reflective access, independent of whether it's exported.
exports governs ordinary compile/run-time type access; opens governs setAccessible(true)-style reflection. They are separate, independently controlled concerns.
Depending on an automatic module's filename-derived name for long-term stability.
Prefer libraries that declare an explicit Automatic-Module-Name in their manifest, or a real module-info.java, and pin exact JAR filenames if relying on the derived name.
A filename-derived automatic module name changes if the JAR is renamed, silently breaking any requires directive naming it.
Forgetting requires transitive when a module's public API exposes types from a dependency.
Add transitive so consuming modules automatically gain the necessary requires without duplicating it themselves.
Without transitive, every downstream module that uses your API's return/parameter types from the dependency must separately declare requires for it, or fail to compile.
What new unit of encapsulation did JPMS (Java 9) introduce, and what file declares it?
The module, declared by a module-info.java file (compiled to module-info.class) at the root of its source tree.
What is the difference between exports and opens in a module-info.java?
exports controls normal compile-time and run-time visibility of a package's public types to other modules. opens controls deep reflective access (e.g. setAccessible(true) on non-public members), which is independent of exports.
What does requires transitive do that plain requires does not?
It re-exposes the dependency to any module that in turn requires this module, so downstream consumers don't need to separately declare requires for it themselves — needed when your public API surfaces types from that dependency.
What happens if you try to access a public class from a package that is not listed in any exports directive?
Compilation fails — the package is inaccessible from outside its module, and its public classes cannot be referenced or imported by any other module, even though the classes themselves are public.
What is an automatic module, and how does it get its name?
A plain JAR (with no module-info.class) placed on the module path instead of the classpath. Its name comes from an Automatic-Module-Name manifest entry if present, otherwise it's derived (somewhat unreliably) from the JAR's filename.
What is the unnamed module, and what code lives in it?
The catch-all module that classpath-based code (with no module-info.java) belongs to; it reads every other module and exports all of its own packages to everyone, preserving pre-Java-9 classpath behavior.
How does JPMS formalize the classic ServiceLoader pattern?
Via uses (declaring a module consumes implementations of a service interface) and provides ... with ... (declaring a module supplies a specific implementation) directives in module-info.java, compiler-checked instead of relying only on a META-INF/services text file.
Write a module-info.java for a module named com.example.reporting that requires java.sql, exports com.example.reporting.api to everyone, and exports com.example.reporting.internal only to com.example.reporting.admin.
module com.example.reporting { requires java.sql; exports com.example.reporting.api; exports com.example.reporting.internal to com.example.reporting.admin; } requires java.sql grants access to java.sql's exported types. The first exports is unqualified (visible to any module that requires com.example.reporting); the second is a qualified export, visible only to the named consumer module.
Which of the two lines in app's code compiles, and why?
Only line (1) compiles. lib exports lib.api, so app (which requires lib) can see PublicThing. lib.internal is not exported by lib's module-info.java, so it is completely inaccessible to app — line (2) fails to compile even though Hidden might be a public class.
Write a module-info.java for a module that both opens its entity package for reflection (e.g. for an ORM) and exports it normally for compile-time use.
module com.example.model { exports com.example.model.entity; opens com.example.model.entity; }
Both directives are needed because they control different things: exports allows other modules to compile against and call entity's public types normally; opens additionally allows reflective frameworks (like an ORM) to use setAccessible(true) on its non-public members, such as private fields populated via reflection.
module app {
requires lib;
}
// lib's module-info.java:
module lib {
exports lib.api;
}
// app code:
lib.api.PublicThing t = new lib.api.PublicThing(); // (1)
lib.internal.Hidden h = new lib.internal.Hidden(); // (2)