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.myapp defines the module name.

  • requires java.sql indicates dependency on another module.

  • exports makes 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.