Skip to main content

12.5 Java Module System (JPMS)

What Is the Module System?

Introduced in Java 9(Project Jigsaw), the Java Platform Module System (JPMS) adds a new level of encapsulation above packages. A module is a named, self-describing collection of packages that explicitly declares what it requires and what it exposes.

Before modules, all public classes across all JARs on the classpath were accessible to each other — there was no real way to hide internal implementation details from other libraries. JPMS solves this.


1. Why Modules?

Problems with the pre-module classpath:

  • Accessibility: Any public class was accessible from anywhere — no internal encapsulation between JARs
  • JAR Hell: Multiple versions of the same library on the classpath caused unpredictable behavior
  • Bloated JRE: The entire JDK (100+ packages) was always loaded, even for small apps
  • No explicit dependencies: You couldn't tell what a JAR needed without running it

JPMS benefits:

  • Strong encapsulation — control exactly what packages are visible
  • Reliable dependencies — explicit requires declarations
  • Smaller deployments — include only the modules you need (jlink)
  • Improved security — internal APIs cannot be accessed accidentally

2. module-info.java

Every module has a module-info.java file at the root of its source directory:

// src/com.example.myapp/module-info.java
module com.example.myapp {
// Dependencies: other modules this module needs
requires java.base; // always implicit — java.lang, java.util, etc.
requires java.net.http; // HTTP client module
requires java.sql; // JDBC module

// Exported packages: which packages OTHER modules can use
exports com.example.myapp.api;
exports com.example.myapp.model;
// com.example.myapp.internal is NOT exported — stays private

// Opens packages for deep reflection (e.g., frameworks like Spring/Hibernate)
opens com.example.myapp.model to com.fasterxml.jackson.databind;
}

3. Core Directives

requires — Declare Dependencies

module com.example.service {
requires java.logging; // normal dependency
requires transitive java.sql; // transitive: modules depending on THIS module
// also get access to java.sql
requires static java.annotation; // compile-time only (optional at runtime)
}

requires transitive— "I expose this module's API in my own API, so my callers need it too."

exports — Control Package Visibility

module com.example.library {
// Export to everyone
exports com.example.library.api;

// Qualified export — only specific modules can access
exports com.example.library.internal to com.example.tests, com.example.tools;

// com.example.library.impl is NOT exported — completely hidden
}

opens — Allow Deep Reflection

exports controls compile-time and runtime access. opens additionally allows reflection to access private members (needed by frameworks like Spring, Hibernate, Jackson):

module com.example.webapp {
exports com.example.webapp.controller; // public API accessible

// Open for reflection (Jackson needs to set private fields)
opens com.example.webapp.dto to com.fasterxml.jackson.databind;

// Open everything to a testing framework
opens com.example.webapp.service to org.mockito;
}

uses and provides — Service Loader

// A service consumer module
module com.example.app {
uses com.example.api.PaymentService; // declares use of a service interface
}

// A service provider module
module com.example.stripe {
requires com.example.api;
provides com.example.api.PaymentService
with com.example.stripe.StripePaymentService; // implementation
}

4. Module Types

TypeDescriptionExample
Named moduleHas module-info.java, on the module pathYour own modules
Automatic moduleJAR on module path without module-info.javaLegacy 3rd-party JARs
Unnamed moduleEverything on the classpathTraditional classpath apps
Platform moduleBuilt-in JDK modulesjava.base, java.sql

5. Multi-Module Project Structure

myproject/
├── src/
│ ├── com.example.common/
│ │ ├── module-info.java
│ │ └── com/example/common/
│ │ ├── model/
│ │ │ └── User.java
│ │ └── util/
│ │ └── StringUtils.java
│ │
│ ├── com.example.service/
│ │ ├── module-info.java
│ │ └── com/example/service/
│ │ └── UserService.java
│ │
│ └── com.example.app/
│ ├── module-info.java
│ └── com/example/app/
│ └── Main.java
// com.example.common/module-info.java
module com.example.common {
exports com.example.common.model;
exports com.example.common.util;
}

// com.example.service/module-info.java
module com.example.service {
requires com.example.common;
exports com.example.service;
}

// com.example.app/module-info.java
module com.example.app {
requires com.example.common;
requires com.example.service;
}

6. Compiling and Running Modular Code

# Compile all modules
javac -d out \
--module-source-path src \
-m com.example.common,com.example.service,com.example.app

# Run the application
java --module-path out -m com.example.app/com.example.app.Main

# Create a minimal custom runtime image (jlink)
jlink \
--module-path $JAVA_HOME/jmods:out \
--add-modules com.example.app \
--output myapp-runtime \
--launcher start=com.example.app/com.example.app.Main

# Run the custom image
./myapp-runtime/bin/start

7. JDK Platform Modules

The JDK itself is organized into modules. Common ones:

ModuleContents
java.baseCore Java (java.lang, java.util, java.io, etc.)
java.sqlJDBC
java.net.httpHTTP Client (Java 11+)
java.loggingjava.util.logging
java.xmlJAXP XML processing
java.desktopAWT, Swing
javafx.*JavaFX UI framework
jdk.jshellJShell REPL

View all available modules:

java --list-modules

Pro Tips

Most applications don't need to use JPMS directly. Spring Boot, Jakarta EE, and similar frameworks handle module compatibility. You'll encounter JPMS mainly when:

  1. Building a library that needs strong API encapsulation
  2. Creating a custom JRE image with jlink
  3. Working with frameworks that use deep reflection (and need opens)

Automatic modules make migration easier: putting a legacy JAR on the --module-path gives it a module name derived from its filename. This lets modular code depend on non-modular JARs during migration.

--add-opens flag: When a framework tries to reflect into a module that doesn't opens the package, you'll see InaccessibleObjectException. Fix it by adding to the JVM arguments:

--add-opens java.base/java.lang=ALL-UNNAMED

This is a common workaround when using older libraries with Java 17+.