The Java Module System (JPMS), introduced in Java 9 as Project Jigsaw, was the largest change to Java's structure in a decade. It introduced modules as a unit larger than packages: a named, versioned, encapsulated unit with explicit imports and exports. The promise: better encapsulation, smaller deployments, faster startup.
The reality: modules are useful for some codebases and overhead for others. Many production Java codebases stay on the classpath. This page is about how modules work, when they pay off, and the practical path for adoption.
A module is declared by a module-info.java file at the root of its source tree:
module com.example.orders {
requires com.example.common;
requires java.sql;
exports com.example.orders.api;
exports com.example.orders.model;
}
This module:
com.example.common and java.sqlapi and model packages to consumersexports is explicit. Code in com.example.orders.internal is not visible to other modules unless explicitly exported. This is real encapsulation — even reflection is restricted unless the module opens packages.
Pre-modules, public was effectively module-public; any code on the classpath could access it. Post-modules, public plus exports is the boundary.
The module graph is verified at startup. Missing dependencies fail fast; conflicting versions are caught. Compare to classpath, where missing classes appear as runtime exceptions far from the cause.
jlink produces a JRE containing only the modules your application uses — sometimes 30–50% smaller than a full JRE. Useful for containerized deployments.
Modules can declare provider/consumer relationships:
provides com.example.SomeService with com.example.MyImpl;
uses com.example.SomeService;
Cleaner than the older META-INF/services mechanism.
jlink matters.module-info.java files in every module exceeds the benefit.Three modes:
For most application code, the classpath approach (no module-info.java) continues to work. Java has full backwards compatibility. The encapsulation features of modules are not available, but neither are the costs.
This is what most Spring Boot applications do. Spring's reflection-heavy approach means modules add friction without compensating benefit.
A JAR without module-info.java becomes an "automatic module" when placed on the module path — its name is derived from the JAR filename. Useful for transitional periods where some dependencies are modular and some are not.
The names are unstable (depend on JAR filenames); for libraries this is dangerous. For applications, automatic modules are a reasonable migration step.
Adding module-info.java to every module. For libraries this is now expected; for applications it is opt-in.
The migration cost: every module gets a module-info.java file with its dependencies and exports. The benefit: strong encapsulation, smaller deployments, reliable configuration.
requires transitiveWhen module A re-exports parts of B that consumers also need:
requires transitive com.example.api;
Saves consumers from having to require the transitive dependency themselves.
opens for reflectionFrameworks need reflective access to non-public members:
opens com.example.entity to com.example.persistence;
Allows persistence frameworks to read private fields. Without this, reflection fails.
For pluggable architectures:
// Provider module
provides com.example.Plugin with com.example.MyPlugin;
// Consumer module
uses com.example.Plugin;
The consumer iterates over ServiceLoader<Plugin> to find providers.
opens. Spring/Hibernate failures are common.requires for things you don't actually use. Overstates the dependency graph.exports too liberally. Defeats encapsulation.For most application development:
Automatic-Module-Name manifest entry indicates a stable name even without module-info)For library development:
Automatic-Module-Name to the manifest as a minimummodule-info.java if your consumers benefit from itThe Java module system is a real feature with real benefits in specific contexts. For most production Java applications, the costs exceed the benefits.