Introduction
When Java 9 was released in September 2017, it was one of the most ambitious releases in Java’s history.
Unlike Java 8, which introduced functional programming features such as Lambdas and Streams, Java 9 focused on the Java platform itself.
Its headline feature was the Java Platform Module System (JPMS), often referred to as Project Jigsaw.
For more than twenty years, Java applications had been organized using packages and JAR files.
A typical enterprise application looked like this:
Customer-Service.jar
Order-Service.jar
Payment-Service.jar
commons.jar
utils.jar
logging.jar
The JVM loaded every class that was present on the classpath.
There was no way to explicitly declare:
- Which libraries were required.
- Which packages were intended for public use.
- Which packages should remain internal.
- Which dependencies were missing.
- Which libraries should be inaccessible to other applications.
As applications grew larger, these limitations became increasingly problematic.
Java needed a stronger way to organize software.
That solution was the Java Platform Module System.
Learning Objectives
By the end of this article, you will be able to:
- Understand why JPMS was introduced.
- Learn the limitations of the classpath.
- Understand modules.
- Create your first module.
- Understand
module-info.java. - Learn module dependencies.
- Understand encapsulation.
- Decide whether JPMS is appropriate for your applications.
The Problem with the Classpath
Before Java 9, every application used the classpath.
java -cp app.jar:commons.jar:mysql.jar Main
The JVM searched every JAR for required classes.
Although simple, this approach introduced several problems.
Problem 1 – Classpath Hell
Imagine two libraries:
Library A
↓
commons-logging-1.0.jar
Library B
↓
commons-logging-2.0.jar
Which version should the JVM load?
The answer depended on classpath ordering.
This often caused unexpected runtime errors.
Problem 2 – No Encapsulation
Suppose a library contains:
com.company.api
and
com.company.internal
Nothing prevented another application from importing:
import com.company.internal.SecretClass;
Even though the package was never intended to be public.
Packages alone could not enforce encapsulation.
Problem 3 – Missing Dependencies
Imagine:
Application
↓
Payment Library
↓
Logging Library
If the logging library was accidentally omitted from the classpath, the application often failed only at runtime.
JPMS enables many dependency issues to be detected earlier.
What Is a Module?
A module is a named collection of related packages together with an explicit declaration of:
- Its dependencies.
- The packages it exports.
- The services it provides.
- The services it consumes.
Think of a module as a JAR with a well-defined contract.
Your First Module
Project structure:
customer-module
│
├── module-info.java
└── com.example.customer
The heart of every module is:
module-info.java
module-info.java
Example:
module customer.module {
exports com.example.customer;
}
This declares:
- The module name.
- The packages visible to other modules.
Everything else remains strongly encapsulated.
Exporting Packages
Suppose the module contains:
com.example.customer.api
com.example.customer.internal
Module descriptor:
module customer.module {
exports com.example.customer.api;
}
Only the API package is visible.
The internal package cannot be accessed by other modules.
This is a significant improvement over the traditional classpath model.
Module Dependencies
Suppose Order Service uses Customer Service.
module order.module {
requires customer.module;
}
The dependency is explicit.
This improves readability and tooling support.
Readability Graph
Order Module
↓
Customer Module
↓
Logging Module
The JVM understands these relationships and enforces module boundaries.
Strong Encapsulation
Before Java 9:
import com.company.internal.Helper;
was technically possible if the class was on the classpath.
With JPMS, only exported packages are accessible.
This protects implementation details and reduces accidental coupling.
Services with JPMS
JPMS includes a built-in service mechanism.
Provider module:
module payment.provider {
provides PaymentGateway
with StripeGateway;
}
Consumer module:
module order.module {
uses PaymentGateway;
}
This supports loosely coupled designs without requiring direct implementation dependencies.
Automatic Modules
Most existing libraries were not modularized when Java 9 was released.
To ease migration, Java introduced automatic modules.
A traditional JAR placed on the module path automatically becomes a module with a derived name.
This allowed gradual adoption rather than requiring a complete rewrite.
Named vs Unnamed Modules
| Module Type | Description |
|---|---|
| Named Module | Has module-info.java |
| Automatic Module | Traditional JAR placed on the module path |
| Unnamed Module | Traditional classpath application |
This flexibility made migration practical for existing applications.
JPMS in Enterprise Applications
Suppose a banking application contains:
Customer Module
Account Module
Loan Module
Card Module
Notification Module
Security Module
Each module explicitly declares:
- Required dependencies.
- Exported APIs.
- Hidden implementation packages.
This improves maintainability in large codebases.
Does Spring Boot Require JPMS?
No.
Spring Boot applications can continue to run on the traditional classpath.
Many enterprise applications still choose this approach because of framework compatibility, third-party libraries, and operational simplicity.
JPMS is a design choice, not a mandatory migration step.
Benefits of JPMS
✔ Strong encapsulation.
✔ Explicit dependencies.
✔ Better tooling.
✔ Improved maintainability.
✔ Smaller runtime images (with tools introduced alongside Java 9).
✔ Better platform organization.
Challenges
JPMS also introduces additional complexity.
Examples include:
- Modularizing large legacy applications.
- Working with libraries that are not fully modular.
- Reflective access restrictions.
- Additional build configuration.
Teams should evaluate these trade-offs before adopting modules.
Migration Strategy
For most enterprise applications:
- Upgrade the JDK.
- Continue using the classpath.
- Resolve compatibility issues.
- Modularize incrementally where it provides clear value.
This phased approach reduces migration risk.
Common Mistakes
Modularizing Too Early
Not every project benefits immediately from JPMS.
Evaluate the architecture and ecosystem before investing in modularization.
Exporting Everything
Export only the packages intended to be part of the public API.
Internal implementation packages should remain encapsulated.
Confusing Packages with Modules
A package organizes related classes.
A module organizes packages and defines explicit dependencies and visibility.
Best Practices
✔ Keep modules cohesive.
✔ Export only public APIs.
✔ Minimize dependencies between modules.
✔ Prefer gradual migration for existing systems.
✔ Review framework compatibility before modularizing.
Interview Questions
Why was JPMS introduced?
To provide strong encapsulation, explicit dependencies, and better organization of large applications.
What file defines a Java module?
module-info.java
What is the difference between a package and a module?
A package groups related classes.
A module groups packages and defines dependency and visibility rules.
Can Spring Boot applications run without JPMS?
Yes. Spring Boot fully supports traditional classpath-based applications.
What is an automatic module?
A traditional JAR placed on the module path that is treated as a module without requiring a module-info.java file.
Summary
The Java Platform Module System represents one of the most significant architectural changes introduced since Java’s inception. By replacing the unrestricted classpath model with explicit modules, Java gained stronger encapsulation, better dependency management, and improved maintainability for large applications.
Although JPMS is not mandatory for every project, understanding its concepts is essential for modern Java developers. Even teams that continue using the classpath benefit from understanding how the platform itself has evolved.
Coming Up Next
Part 24 – Java 9 Collection & Stream Enhancements
We’ll explore the practical improvements Java 9 brought to everyday development, including:
List.of()Set.of()Map.of()Map.ofEntries()Stream.takeWhile()Stream.dropWhile()Stream.iterate()Optional.stream()Stream.ofNullable()
We’ll compare each enhancement with its Java 8 equivalent and demonstrate how these APIs simplify enterprise application development.