Skip to main content
Java advanced Lesson 38 of 58

Java Module System (JPMS)

Master the Java Platform Module System — module-info.java, exports, requires, opens, services, and modular application structure.

Why Modules?

Before the Java Platform Module System (JPMS, Java 9+), the JDK itself was one massive classpath JAR — rt.jar — and there was no way to prevent other code from using internal JDK classes like sun.misc.Unsafe. JPMS introduces:

  • Strong encapsulation — packages are hidden by default; only exported packages are accessible
  • Explicit dependencies — each module declares exactly what it needs
  • Reliable configuration — missing or duplicate modules are detected at startup, not at runtime
  • Reduced footprint — custom JVM images can include only the modules needed

Module Basics

A module is a named group of packages declared in module-info.java at the module root:

src/
└── com.example.app/
    ├── module-info.java
    └── com/example/app/
        └── Main.java
// module-info.java — must be at the source root of the module
module com.example.app {
    requires java.net.http;          // depend on JDK HTTP client module
    requires com.example.utils;      // depend on another app module
    exports com.example.app.api;     // make this package public to other modules
}

A Complete Multi-Module Example

project/
├── modules/
│   ├── com.example.api/
│   │   ├── module-info.java
│   │   └── com/example/api/
│   │       └── UserService.java
│   ├── com.example.impl/
│   │   ├── module-info.java
│   │   └── com/example/impl/
│   │       └── UserServiceImpl.java
│   └── com.example.app/
│       ├── module-info.java
│       └── com/example/app/
│           └── Main.java

API module — defines the contract

// modules/com.example.api/module-info.java
module com.example.api {
    exports com.example.api; // expose the interface
}
// com/example/api/UserService.java
package com.example.api;

import java.util.List;
import java.util.Optional;

public interface UserService {
    Optional<User> findById(long id);
    List<User> findAll();
    User save(User user);
}

public record User(long id, String name, String email) {}

Implementation module

// modules/com.example.impl/module-info.java
module com.example.impl {
    requires com.example.api;  // depends on the API
    exports com.example.impl;  // expose the implementation class
}
// com/example/impl/UserServiceImpl.java
package com.example.impl;

import com.example.api.User;
import com.example.api.UserService;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

public class UserServiceImpl implements UserService {
    private final Map<Long, User> store = new HashMap<>();
    private final AtomicLong ids = new AtomicLong(1);

    @Override public Optional<User> findById(long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override public List<User> findAll() {
        return List.copyOf(store.values());
    }

    @Override public User save(User user) {
        var saved = new User(ids.getAndIncrement(), user.name(), user.email());
        store.put(saved.id(), saved);
        return saved;
    }
}

Application module

// modules/com.example.app/module-info.java
module com.example.app {
    requires com.example.api;
    requires com.example.impl;
}
// com/example/app/Main.java
package com.example.app;

import com.example.api.User;
import com.example.api.UserService;
import com.example.impl.UserServiceImpl;

public class Main {
    public static void main(String[] args) {
        UserService svc = new UserServiceImpl();

        svc.save(new User(0, "Alice", "alice@example.com"));
        svc.save(new User(0, "Bob",   "bob@example.com"));

        svc.findAll().forEach(System.out::println);
        // User[id=1, name=Alice, email=alice@example.com]
        // User[id=2, name=Bob,   email=bob@example.com]
    }
}

exports Directive

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

    // Qualified export — only to specific modules
    exports com.example.lib.internal to com.example.app, com.example.tests;

    // Internal packages are hidden by default — no exports needed
    // com.example.lib.impl is completely inaccessible to other modules
}

opens Directive — Reflection Access

Frameworks like Spring, Jackson, and Hibernate need deep reflective access (setting private fields, invoking private constructors). Use opens:

module com.example.model {
    requires java.base;

    // Open for reflection at runtime (Jackson, Hibernate, Spring)
    opens com.example.model.entity to com.fasterxml.jackson.databind;
    opens com.example.model.dto    to org.springframework.core;

    // Open to everyone — use sparingly
    opens com.example.model.config;

    // Exports the package for normal compilation use
    exports com.example.model.entity;
    exports com.example.model.dto;
}

Services — Decoupled Module Communication

The service loader mechanism lets modules provide and consume services without compile-time dependencies:

// API module — declares the service interface
module com.example.api {
    exports com.example.api;
    uses com.example.api.PaymentProcessor; // declares it consumes this service
}

// Provider module — implements the service
module com.example.stripe {
    requires com.example.api;
    provides com.example.api.PaymentProcessor
        with com.example.stripe.StripeProcessor; // registers the implementation
}

// Consumer — load at runtime via ServiceLoader
import java.util.ServiceLoader;
import com.example.api.PaymentProcessor;

ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
PaymentProcessor processor = loader.findFirst()
    .orElseThrow(() -> new RuntimeException("No PaymentProcessor found"));
processor.charge(9.99, "USD");

Compiling and Running Modules

# Compile each module
javac -d out/com.example.api \
    --module-source-path modules \
    modules/com.example.api/module-info.java \
    modules/com.example.api/com/example/api/*.java

javac -d out/com.example.impl \
    --module-path out \
    modules/com.example.impl/module-info.java \
    modules/com.example.impl/com/example/impl/*.java

javac -d out/com.example.app \
    --module-path out \
    modules/com.example.app/module-info.java \
    modules/com.example.app/com/example/app/*.java

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

jlink creates a minimal JVM image containing only the modules your application needs:

# Find which JDK modules your app uses
jdeps --module-path out -m com.example.app

# Build a custom JRE containing only required modules
jlink \
    --module-path $JAVA_HOME/jmods:out \
    --add-modules com.example.app,com.example.impl,com.example.api \
    --output custom-jre \
    --compress zip-6 \
    --no-header-files \
    --no-man-pages

# Run with the custom JRE (no JDK needed on target machine)
custom-jre/bin/java -m com.example.app/com.example.app.Main

A typical Spring Boot app cut to only needed modules can shrink from a 200 MB JDK to a 40-60 MB runtime image.

Module Descriptor Quick Reference

module com.example.full {
    // Declare dependencies
    requires java.sql;                // compile + runtime
    requires transitive java.logging; // transitive: consumers of this module also get java.logging
    requires static java.compiler;    // optional at runtime (only needed at compile time)

    // Expose packages for compilation and runtime
    exports com.example.full.api;
    exports com.example.full.spi to com.example.plugins; // qualified

    // Expose packages for deep reflection
    opens com.example.full.model;
    opens com.example.full.model to com.fasterxml.jackson.databind; // qualified

    // Service declarations
    uses com.example.full.spi.Plugin;
    provides com.example.full.spi.Plugin with com.example.full.DefaultPlugin;
}

Frequently Asked Questions

Do I need to use modules for every project?
No. Modules are optional. The classpath still works exactly as before in the 'unnamed module'. Modules are most valuable for large applications or libraries where strong encapsulation, explicit dependencies, and reduced attack surface matter. Small applications and Spring Boot projects typically stay on the classpath.
What is the difference between exports and opens?
'exports com.example.api' makes the package's public types visible to other modules at compile time and runtime. 'opens com.example.model' grants deep reflective access at runtime (needed by frameworks like Jackson, Hibernate, Spring) but does not make the package visible for normal compilation.
What is a split package and why is it forbidden?
A split package occurs when the same package name exists in two different modules. The module system forbids this because it would be ambiguous which module should supply a type from that package. This is stricter than the classpath, which allowed split packages (causing subtle bugs).
How do I use a library that is not yet modularised?
Non-modular JARs on the module path are treated as 'automatic modules'. Their module name is derived from the JAR filename (e.g., guava-32.0.0.jar becomes guava). You can require them by this derived name. JARs still on the classpath go into the unnamed module, which is readable by all named modules.