Java : Platfrom Module System
In this article, we will explore the fundamental concepts of Java Platform Module System (JPMS), understand the problems it solves, and look at practical examples of how to define, export, and wire modules together effectively.
What are Modules?
The evolution of Java from Java 8 to Java 9 introduced an important new feature called Java Platform Module System (JPMS) which is also called Project Jigsaw. This feature was introduced to solve the problem of dependency management in large Java applications, improve development efficiency, boost performance, simplify the JRE and enhance compatibility and security.
Benefits of Modules
Efficient Dependency Management
- Each module includes a module descriptor file that explicitly lists the required modules and the list of packages that are exposed to other modules. This allows the Java runtime to build a module graph at startup, ensuring all required components are present before execution begins.
- JPMS does not allow different modules to export the same package, Hence Java can accurately locate the given class and package when searching for the dependency, thus achieving better performance.
Optimized Java Runtime Environment
- After the introduction of JPMS the JDK itself was divided into various different modules. Using the
jlinktool (which was also introduced in Java 9), developers can generate a custom lightweight JRE by combining only the modules required by the application. Smaller JRE means less memory usage, which makes Java more friendly for embedded application development.
Improved Security
- Before JPMS was introduced a public class was accessible to any other classes on the class path, but when using a module a class is accessible only when the package containing the class is explicitly exported by the providing module and the consuming module explicitly requires the providing module in the module descriptor.
- Modules block deep reflection access by default. Java Reflection API can be used to bypass the
privateandprotectedmodifiers which restricts access to internal data.
Defining Modules
The heart of a Java module is the module descriptor which corresponds to the module-info.class file in the root directory. This class file is compiled from the module-info.java present in the root directory of the source code. The module descriptor has a dedicated syntax.
[open] module <module-name> {
//
exports <package-name> [to <module-name-1>[, <module-name-2> ...]];
//
requires [transitive] [static] <module-name>;
//
opens <package-name> [to <module-name-1>[, <module-name-2>...]]
//
provides <interface | abstract class> with <class-1>[, <class-2> ...]
//
uses <interface | abstract class>
}Module Descriptor Syntax
[open] module <module> — A Java module can be declared using the module keyword followed by the module name. The module name is made up of letters, digits, underscores and periods.
- Optionally, the
openmodifier can be used to indicate that all packages within the module are allowed to be accessed through Java Reflection API.
exports <package> [to <module-1>[, <module-2> ...]] — Declares the package within the module that is allowed to be imported by the consuming module. A package that is not exported is not accessible outside the module.
- The scope of the exported package can be narrowed down by using the
tomodifier followed by the list of consuming modules that are allowed to access the package. Such a statement is called a qualified export.
opens <package> [to <module-1>[, <module-2> ...]] — Declares the package within the module that are allowed access using Java Reflection API. A package that is not opened is not accessible using Java Reflection API.
- The scope of the opened package can be narrowed down by using the
tomodifier followed by the list of consuming modules that are allowed to access the package using Java Reflection API. Such a statement is called a qualified open.
requires [transitive] [static] <module> — Declares the dependent module name from which packages are imported and used.
- Sometimes, packages from the transitive dependency of a dependent module might be required. In such cases the
transitivemodifier can be used to automatically require the transitive dependencies. - Sometimes, a module is required only during compile time but could be optional at runtime. In such cases the
staticmodifier can be used. There are two use cases where this could come in handy — When accessing an annotation that is processed at compile time and when a class is used if available and otherwise takes a different course of action.
provides <interface | abstract class> with <class-1>[, <class-2> ...] — Declares the implementation classes provided by the module for the Service Provider Interface (SPI).
uses <interface | abstract class> — Declares the Service Provider Interface (SPI) the module depends on, i.e. The code in the module loads the SPI implementation using ServiceLoader.load(<interface | abstract class>).
Modular JARs
Java module can be deployed by placing all its classes in a JAR file, along with the module-info.class file in the root. Such a JAR file is called a modular JAR.
module com.codewithguhan.greeter {
exports com.codewithguhan.greeter.spi;
provides com.codewithguhan.greeter.spi.Greeter with
com.codewithguhan.greeter.internal.EnglishGreeter,
com.codewithguhan.greeter.internal.FrenchGreeter;
}# Compile the java source
javac -d modules/greeter \
greeter/module-info.java \
greeter/com/codewithguhan/greeter/internal/FrenchGreeter.java \
greeter/com/codewithguhan/greeter/internal/EnglishGreeter.java \
greeter/com/codewithguhan/greeter/spi/Greeter.java \
greeter/com/codewithguhan/greeter/Main.java
# Package the java classes
jar -c -v -f greeter.jar \
-e com.codewithguhan.greeter.Main \
-C modules/greeter
# Launching the program
java --module-path greeter.jar -m com.codewithguhan.greeterImportant Note — While using module JAR, classes of a package cannot be distributed over multiple JAR files, i.e. Split Packaging is not possible.
While creating a module JAR, optionally a version number can be provided using the --module-version command line flag. The version number is not used by the JPMS for resolving modules, but it can be queried by third-party tools and frameworks.
# Package the java classes
jar -c -v -f greeter-1.0.0.jar \
-e com.codewithguhan.greeter.Main \
-C modules/greeter \
--module-version 1.0.0Automatic Modules & Unnamed Modules
When building an application from the ground up, you have full control over the codebase and can make it entirely modular by providing a module descriptor for each component. Such modules are called Named Modules or Explicit Modules.
Get Guhan B’s stories in your inbox
Join Medium for free to get updates from this writer.
However, real-world applications rarely exist in isolation — they depend on third-party libraries, and there is no guarantee that those libraries ship as named modules. To bridge this gap, JPMS provides two mechanisms for integrating non-modular dependencies into a modular application: Automatic Modules and Unnamed Modules.
Automatic Modules
When a JAR without a module-info.class is placed on the module path, it becomes an automatic module. An automatic module has the following properties.
- The module implicitly has a
requiresclause for all other modules. - All of its packages are exported and opened.
- If the JAR file manifest
META-INF/MANIFEST.MFcontains a keyAutomatic-Module-Namethen its value becomes the name of the module, or else the module name is obtained from the JAR file name by dropping any trailing version number and replacing any sequence of non-alphanumeric characters with a dot. (e.g.greeter-spi-1.0.0.jar—greeter.spi)
Unnamed Modules
Packages that are not on the module path is part of an unnamed module. An unnamed modules has the following properties.
- An unnamed module can access all other modules and all of its packages are exported and opened.
- An explicit module cannot access the unnamed module.
Modules & Reflective Access
Modules enforce strict encapsulation, i.e. a module can only access explicitly exported packages from another module, similarly reflective access to non-public fields of a class is also restricted. In the non-modular world it is possible to break this access restriction easily by using Java Reflection API. For example, private field of an object can be accessed and manipulated in the following way by using reflection.
Field field = object.getDeclaredField("score");
field.setAccessible(true);
int value = field.getInt(object);
field.setInt(object, score * 2);However, in the module world this is no longer possible. When reflective access is attempted on non-public fields the JVM throws an InaccessibleObjectException at runtime.
To allow reflective access to a package, the providing module must explicitly open it using the opens directive in its module descriptor. To restrict this access to specific modules only, a qualified opens directive can be used. For cases where reflective access needs to be granted to all packages within a module, the entire module can be declared as open.
// Option 1 — Open to all modules for reflection
module com.codewithguhan.greeter {
opens com.codewithguhan.greeter.model;
}
// Option 2 — Qualified open (restricted to specific modules only)
module com.codewithguhan.greeter {
opens com.codewithguhan.greeter.model to com.fasterxml.jackson.databind;
}
// Option 3 — Open module (all packages open for reflection)
open module com.codewithguhan.greeter {
// all packages are implicitly open for reflection
}Important Note — Many popular frameworks such as Spring, Hibernate, Jackson and JAX-B rely heavily on Java Reflection API to inspect and manipulate classes at runtime. When running these frameworks in a modular environment, it is common to see
InaccessibleObjectExceptionerrors if the relevant packages are not opened. The recommended approach is to use qualified opens in the module descriptor to grant access only to the specific framework modules that require it, keeping encapsulation as tight as possible.
In situations where you cannot modify the module descriptor of a dependency — for example, when using a JDK internal module or when using a third-party dependency — the --add-opens command-line flag can be used to grant reflective access at launch time.
java --add-opens java.base/java.lang=com.codewithguhan.greeter -m com.codewithguhan.greeterImportant Note — This should be treated as a temporary workaround during migration, not a long-term solution, as it bypasses the encapsulation JPMS is designed to enforce.
Modules & Service Loading
The ServiceLoader class provides an elegant mechanism for matching up Service Provider Interfaces (SPI) with their implementations at the runtime.
Before JPMS, the implementation classes are configured by placing a text file under META-INF/services/ inside the JAR. The file was named after the fully qualified service interface, and its contents listed the fully qualified implementation classes, one per line.
META-INF/
`-- services/
`-- com.codewithguhan.greeter.spi.Greetercom.codewithguhan.greeter.internal.EnglishGreeter
com.codewithguhan.greeter.internal.FrenchGreeterThis approach worked, but it had no formal contract — nothing in the code explicitly declared that a JAR was providing or consuming a service. The relationship was implicit, buried in a text file, and invisible to the compiler.
JPMS brings service relationships into the module descriptor, making them explicit, verifiable, and visible to the module system at startup. The provider module declares its implementations using the provides directive, and the consumer module declares its intent to load the service using the uses directive — both in their respective module-info.java files.
// Provider module-info.java
module com.codewithguhan.greeter {
provides com.codewithguhan.greeter.spi.Greeter
with com.codewithguhan.greeter.internal.EnglishGreeter,
com.codewithguhan.greeter.internal.FrenchGreeter;
}// Consumer module-info.java
module com.codewithguhan.greeter.app {
requires com.codewithguhan.greeter.spi;
uses com.codewithguhan.greeter.spi.Greeter;
}Important Note — The provider module does not need to export the package containing the implementation classes. The
providesdirective givesServiceLoaderprivileged access to instantiate them without exposing the internals to other modules, which keeps encapsulation intact.
Conclusion
In this article, We explored Platform Module System in Java — The Java Platform Module System is a significant step forward on how Java application are developed and maintained. By making dependencies explicit through module descriptors, enforcing string encapsulation, and formalizing service relationships, JPMS brings discipline and clarity to larger codebases.










