Java - Java Module System (JPMS) and Modular Programming
The Java Module System, also known as JPMS (Java Platform Module System), was introduced in Java 9 to improve the structure, security, maintainability, and scalability of Java applications. Before Java 9, Java projects were organized mainly using packages and JAR files. Although packages helped group related classes, there was no strong mechanism to control dependencies between different parts of an application. This often created problems such as class conflicts, large application sizes, and poor dependency management.
JPMS solves these issues by introducing modules as a higher-level organizational unit above packages. A module is a collection of related packages and resources with a clearly defined interface and dependency list. It allows developers to specify which packages are accessible to other modules and which remain hidden internally.
What is a Module in Java
A module is a self-contained unit of code that contains:
-
Packages
-
Classes
-
Interfaces
-
Resources
-
Dependency declarations
Every module must contain a special file called module-info.java. This file describes the module’s metadata, dependencies, and exported packages.
Basic structure:
module com.example.myapp {
requires java.sql;
exports com.example.myapp.services;
}
In this example:
-
module com.example.myappdefines the module name. -
requires java.sqlindicates dependency on another module. -
exportsmakes a package accessible to other modules.
Why JPMS Was Introduced
Before JPMS, Java applications faced several challenges:
1. Classpath Problems
Large applications often suffered from “classpath hell,” where multiple JAR files contained conflicting versions of the same classes.
Example problems:
-
Duplicate libraries
-
Missing dependencies
-
Runtime errors due to incompatible versions
JPMS replaces the classpath with the module path, providing stronger dependency management.
2. Lack of Encapsulation
Packages could not completely hide internal implementation details. Any public class could be accessed from anywhere.
JPMS allows modules to expose only selected packages while hiding internal code.
3. Large Runtime Size
The entire Java Runtime Environment (JRE) was included even if only a few libraries were required.
With modules, developers can build custom lightweight runtimes containing only necessary modules.
Key Features of JPMS
1. Strong Encapsulation
Only explicitly exported packages are visible outside the module.
Example:
module banking.app {
exports com.bank.api;
}
Packages not exported remain inaccessible to other modules.
This improves:
-
Security
-
Maintainability
-
Internal code protection
2. Explicit Dependencies
Modules clearly declare required modules.
Example:
module inventory.system {
requires java.sql;
requires java.logging;
}
This makes dependency relationships transparent and easier to maintain.
3. Reliable Configuration
JPMS checks dependencies during compilation and startup.
It can detect:
-
Missing modules
-
Circular dependencies
-
Duplicate packages
This reduces runtime failures.
4. Better Performance
Applications load only required modules, improving startup time and reducing memory usage.
5. Custom Runtime Images
Using the jlink tool, developers can create smaller runtime environments.
Example:
jlink --module-path $JAVA_HOME/jmods \
--add-modules com.example.app \
--output custom-runtime
This is useful for:
-
Cloud applications
-
Embedded systems
-
Microservices
Module Types in Java
JPMS supports different module types.
1. Named Modules
Modules with module-info.java.
Example:
module com.shop.cart {
}
2. Automatic Modules
Existing JAR files without module descriptors can still work on the module path.
Java automatically creates module names for them.
3. Unnamed Modules
Traditional classpath-based applications belong to the unnamed module.
This ensures backward compatibility.
Understanding module-info.java
This file is the heart of a Java module.
Example:
module com.company.employee {
requires java.sql;
exports com.company.employee.service;
opens com.company.employee.model;
}
Important Keywords
requires
Declares dependency on another module.
requires java.sql;
exports
Makes packages available to other modules.
exports com.company.employee.service;
opens
Allows runtime reflection access.
Used by frameworks like:
-
Hibernate
-
Spring
opens com.company.employee.model;
uses
Specifies service usage.
uses com.company.payment.PaymentService;
provides
Provides implementation of a service.
provides PaymentService with PayPalService;
Creating a Simple Modular Application
Step 1: Create Module Structure
src
└── com.mathapp
├── module-info.java
└── com/mathapp
└── Calculator.java
Step 2: Create Calculator Class
package com.mathapp;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Step 3: Create module-info.java
module com.mathapp {
exports com.mathapp;
}
Step 4: Compile Module
javac -d out --module-source-path src $(find src -name "*.java")
Step 5: Run Application
java --module-path out -m com.mathapp/com.mathapp.Main
Module Path vs Classpath
Classpath
-
Old dependency system
-
Weak encapsulation
-
No dependency validation
Module Path
-
Modern dependency management
-
Strong encapsulation
-
Dependency checking
JPMS encourages using the module path instead of the classpath.
Benefits of Modular Programming
1. Improved Maintainability
Applications become easier to manage because modules separate responsibilities clearly.
2. Better Security
Internal packages remain hidden from external access.
3. Easier Testing
Modules can be tested independently.
4. Scalability
Large enterprise applications become more organized.
5. Faster Deployment
Smaller runtime images reduce deployment size.
Challenges of JPMS
Although JPMS offers many advantages, it also introduces challenges.
1. Migration Complexity
Older applications may require significant restructuring.
2. Third-Party Library Compatibility
Some older libraries are not fully modularized.
3. Reflection Restrictions
Frameworks using reflection may require additional configuration using opens.
JPMS in Enterprise Applications
JPMS is highly useful in:
-
Banking systems
-
Cloud-native applications
-
Microservices
-
Large enterprise software
It helps maintain clean architecture and prevents dependency chaos.
JPMS and Microservices
Microservices often contain many independent services. JPMS improves modularity by:
-
Isolating business logic
-
Reducing dependency conflicts
-
Creating lightweight runtimes
This aligns well with containerized deployments using Docker and Kubernetes.
Best Practices for JPMS
1. Use Meaningful Module Names
Example:
com.company.billing
2. Export Only Necessary Packages
Avoid exposing internal packages.
3. Avoid Cyclic Dependencies
Design modules independently.
4. Keep Modules Small and Focused
Each module should have a single responsibility.
5. Use jlink for Production
Create optimized runtimes for deployment.
Conclusion
The Java Module System is one of the most important additions to modern Java development. It introduces a structured and reliable way to organize applications using modules instead of only packages and JAR files. JPMS improves dependency management, encapsulation, performance, and application maintainability.
By using modular programming, developers can build scalable and secure applications that are easier to maintain and deploy. Although adopting JPMS may require changes in older projects, it provides significant long-term advantages for enterprise and cloud-based Java applications.