Using JPMS (Java Platform Module System)

📚 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 on java.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."



Previous
Next Post »