📚 Using JPMS (Java Platform Module System): A Complete Guide
The Java Platform Module System (JPMS) was introduced in Java 9 under Project Jigsaw to solve long-standing problems in Java's dependency and packaging system.
Before JPMS, Java applications often struggled with "classpath hell" — managing large codebases without clear boundaries or control over dependencies.
With JPMS, Java applications became more modular, scalable, and secure.
In this blog post, we will explore:
-
What is JPMS?
-
Why use JPMS?
-
Key Concepts of JPMS
-
How to create a simple module
-
Advanced Features of JPMS
-
Common mistakes and best practices
-
Conclusion
🚀 What is JPMS?
JPMS stands for Java Platform Module System.
It allows you to divide your application into modules — separate, logical units of code that declare:
-
What they export (make available to others)
-
What they require (depend on from others)
Each module explicitly defines its dependencies and APIs.
At the core, JPMS helps in:
-
Encapsulation of internal code
-
Clear dependency management
-
Faster application startup (smaller module graph)
-
Better security and maintainability
🎯 Why Use JPMS?
Let's look at the real-world benefits:
Without JPMS | With JPMS |
---|---|
Large, flat classpath with potential conflicts | Explicitly defined modules with strong encapsulation |
No control over what classes are accessible | Only exported classes are accessible |
Difficult to detect missing dependencies | JVM validates module dependencies at startup |
Hard to scale applications cleanly | Easier to maintain and evolve large codebases |
In short: JPMS = structure + safety + scalability.
📚 Key Concepts of JPMS
Before jumping into examples, let's understand some important terms:
-
module — A named collection of code (packages).
-
module-info.java — A special file that describes the module's metadata (name, dependencies, exports).
-
exports — Keyword to specify which packages are made accessible to other modules.
-
requires — Keyword to specify module dependencies.
-
opens — Keyword to allow reflection-based access.
-
provides/uses — Service loader mechanism to allow pluggable modules.
A simple module-info.java
looks like this:
module com.example.myapp {
requires java.sql;
exports com.example.myapp.service;
}
Here:
-
The module
com.example.myapp
depends onjava.sql
. -
It makes the
com.example.myapp.service
package available to others.
🛠 How to Create a Simple Module
Let's walk through building a small modular Java app.
1. Create the project structure
/modular-app
└── src
├── com.example.greetings
│ ├── module-info.java
│ └── com
│ └── example
│ └── greetings
│ └── Greeting.java
└── com.example.mainapp
├── module-info.java
└── com
└── example
└── mainapp
└── Main.java
2. Define Greeting.java
package com.example.greetings;
public class Greeting {
public static String getMessage() {
return "Hello from Greeting Module!";
}
}
3. Define module-info.java
for Greetings Module
module com.example.greetings {
exports com.example.greetings;
}
It exports the com.example.greetings
package.
4. Define Main.java
package com.example.mainapp;
import com.example.greetings.Greeting;
public class Main {
public static void main(String[] args) {
System.out.println(Greeting.getMessage());
}
}
5. Define module-info.java
for Main App
module com.example.mainapp {
requires com.example.greetings;
}
The main app explicitly requires the greetings
module.
⚙️ How to Compile and Run
From the root directory:
# Create output directory
mkdir -p out/greetings out/mainapp
# Compile greetings module
javac -d out/greetings src/com.example.greetings/module-info.java src/com.example.greetings/com/example/greetings/Greeting.java
# Compile mainapp module
javac --module-path out/greetings -d out/mainapp src/com.example.mainapp/module-info.java src/com.example.mainapp/com/example/mainapp/Main.java
# Run the app
java --module-path out/greetings:out/mainapp -m com.example.mainapp/com.example.mainapp.Main
Output:
Hello from Greeting Module!
🎉 You have successfully built and run your first modular Java application!
🌟 Advanced JPMS Features
JPMS is not just about requires
and exports
. It also supports:
1. Transitive Dependencies
If a module depends on another and wants its consumers to also have access to it:
requires transitive com.example.common;
Useful in layered libraries.
2. Qualified Exports
You can export a package only to specific modules:
exports com.example.secret to com.example.mainapp;
3. Opening Packages for Reflection
Frameworks like Hibernate need reflective access.
opens com.example.entity to hibernate.core;
opens
allows reflection without exposing API.
4. Service Loader Integration
For service discovery:
provides com.example.spi.PaymentService with com.example.impl.PayPalPaymentService;
uses com.example.spi.PaymentService;
A powerful way to build pluggable systems.
⚡ Common Mistakes and Best Practices
Mistake | Best Practice |
---|---|
Exposing too many packages | Export only necessary packages |
Ignoring service loading mechanisms | Use provides/uses properly |
Large monolithic modules | Keep modules small and focused |
Hardcoding module dependencies | Consider flexible service loading |
Forgetting about reflective needs | Use opens carefully |
💡 Tip: Start modularizing your code slowly. Begin with major components before fully modularizing everything.
📝 Conclusion
JPMS is a game-changer for Java developers.
It gives you explicit control over your application's structure, leading to better security, maintainability, and performance.
While it may seem overwhelming at first, the benefits are immense, especially for large or evolving applications.
"Modularity is not just an option anymore — it’s the path forward for clean, scalable Java applications."
Sign up here with your email
ConversionConversion EmoticonEmoticon