DEV Community

Cover image for The Java Platform Module System — Part 1: Fundamentals
9mac-dev
9mac-dev

Posted on

The Java Platform Module System — Part 1: Fundamentals

Part 1 of 4 — A practical introduction to JPMS for senior developers who have never touched module-info.java.

Java 9 shipped the Java Platform Module System nine years ago. Most enterprise projects still run on the classpath — Spring Boot, Jakarta EE, and the major frameworks support modules but do not require them. Yet modules solve real problems: strong encapsulation across JAR boundaries, reliable dependency resolution at startup, and the foundation for custom runtimes with jlink. This article covers the fundamentals — module-info.java, core directives, and strong encapsulation — through a working seven-module Library project.


Why This Matters

For decades, the classpath treated every JAR as a flat search path. Duplicate packages? Silently merged. Missing dependencies? Discovered at runtime. Every public class in every JAR was accessible to all code — no way to hide implementation details across module boundaries.

Oracle faced the same problem inside the JDK itself. By 2008 the platform had grown into a 150 MB monolith where internal APIs like sun.misc.Unsafe were depended on by half the ecosystem. Project Jigsaw took nine years to ship with Java 9, solving three problems:

  1. Strong encapsulation — only explicitly exported packages are accessible
  2. Reliable configuration — missing modules and split packages fail at startup
  3. Scalable platform — ship only the JDK parts your application needs

In 2026, module adoption among application developers remains low. Industry reports track Java versions and frameworks but not module usage. That does not mean modules are useless — their value is concentrated in specific scenarios: library design, plugin architectures, framework internals, and container optimization.


What Is the Module System?

A module is a named, self-describing unit that groups packages and declares its dependencies and exports in a single file: module-info.java. The compiler produces module-info.class, and a JAR containing it is a modular JAR.

Before Java 9 (classpath):

// Any public class in any JAR is accessible everywhere
import com.example.internal.IsbnValidator; // works if the JAR is on the classpath
Enter fullscreen mode Exit fullscreen mode

After Java 9 (module path):

// library-api/src/main/java/module-info.java
module dev.nmac.library.api {
    exports dev.nmac.library.api;
}
Enter fullscreen mode Exit fullscreen mode

Everything not explicitly exports-ed is encapsulated — regardless of Java access modifiers.


Simple Example

The simplest module in the Library project exports a single package with a Book record:

// library-api — module-info.java
module dev.nmac.library.api {
    exports dev.nmac.library.api;
}
Enter fullscreen mode Exit fullscreen mode
// library-api — Book.java
public record Book(String title, String author, String isbn, int year) {

    public Book {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("Title must not be blank");
        }
        if (isbn == null || isbn.isBlank()) {
            throw new IllegalArgumentException("ISBN must not be blank");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
var book = new Book("Effective Java", "Joshua Bloch", "978-0134685991", 2018);
System.out.println(book.title() + " by " + book.author());
// Effective Java by Joshua Bloch
Enter fullscreen mode Exit fullscreen mode

Any module that requires dev.nmac.library.api can use Book. Any module that does not — cannot.


Core Directives

Directive Purpose
requires Declares a dependency on another module
exports Makes a package accessible to other modules
opens Allows deep reflective access at runtime
requires static Optional dependency — required at compile time, optional at runtime
requires transitive Propagates readability to consumers of your module
uses Declares a service consumed via ServiceLoader
provides...with Registers a service implementation
exports...to / opens...to Qualified visibility — restricted to named modules

Key Insight

The exports directive replaces the classpath's implicit rule that every public class is accessible to all code. With modules, a package must be explicitly exported — everything else is encapsulated. public no longer means "accessible everywhere."

For code in module A to access a type in module B, all three must hold:

  1. The type is public
  2. Its package is exports-ed by module B
  3. Module A requires module B (directly or transitively)

More Advanced Examples

Strong Encapsulation

IsbnValidator lives in a non-exported internal package. It is public, but other modules cannot import it:

// library-app — this will NOT compile
import dev.nmac.library.core.internal.IsbnValidator;
Enter fullscreen mode Exit fullscreen mode
error: package dev.nmac.library.core.internal is not visible
    (package dev.nmac.library.core.internal is declared in module
     dev.nmac.library.core, which does not export it)
Enter fullscreen mode Exit fullscreen mode

requires transitive — Implied Readability

When library-core exposes Book types in its public API, consumers need access to library-api too:

// library-core — module-info.java
module dev.nmac.library.core {
    requires transitive dev.nmac.library.api;
    exports dev.nmac.library.core;
    exports dev.nmac.library.core.search to dev.nmac.library.app;
}
Enter fullscreen mode Exit fullscreen mode

library-app only writes requires dev.nmac.library.core — it automatically gains access to library-api types through implied readability.

opens — Reflection Across Module Boundaries

Frameworks like Jackson and Hibernate need setAccessible(true) on private fields. exports alone is not enough:

// library-data — module-info.java
module dev.nmac.library.data {
    requires dev.nmac.library.api;
    exports dev.nmac.library.data;   // compile-time access
    opens dev.nmac.library.data;    // runtime reflective access
}
Enter fullscreen mode Exit fullscreen mode

Without opens, the JVM throws InaccessibleObjectException at runtime.

Service Discovery — uses and provides...with

Decouple consumers from implementations without a requires edge:

// library-app — consumer
module dev.nmac.library.app {
    uses dev.nmac.library.api.BookFormatter;
}

// library-format-provider — provider (no exports needed)
module dev.nmac.library.format.provider {
    requires dev.nmac.library.api;
    provides dev.nmac.library.api.BookFormatter
        with dev.nmac.library.format.provider.PlainTextBookFormatter,
             dev.nmac.library.format.provider.JsonBookFormatter;
}
Enter fullscreen mode Exit fullscreen mode
ServiceLoader<BookFormatter> formatters = ServiceLoader.load(BookFormatter.class);
for (BookFormatter f : formatters) {
    System.out.println("[" + f.name() + "] " + f.format(book));
}
Enter fullscreen mode Exit fullscreen mode

Optional Dependencies — requires static

module dev.nmac.library.app {
    requires static dev.nmac.library.analytics;  // optional at runtime
}
Enter fullscreen mode Exit fullscreen mode

The application compiles against analytics but starts whether or not the module is on the module path — with defensive NoClassDefFoundError handling at runtime.


Classpath vs Module Path

Aspect Classpath Module Path
Dependency declaration Implicit (JARs on path) Explicit (requires in module-info.java)
Public class access All public classes visible Only exports-ed packages visible
Missing dependency NoClassDefFoundError at runtime Fails at startup during resolution
Split packages Silently merged Detected and rejected
Reflection into internals Always allowed Requires opens or --add-opens
module-info.class Ignored Enforced

Real-World Use Cases

  1. Library API design — draw clear boundaries between public API and internal implementation
  2. Plugin architecturesuses / provides...with with ServiceLoader, no compile-time coupling
  3. Framework integrationopens packages for Jackson, Hibernate, or Gson reflection
  4. Optional featuresrequires static for analytics, monitoring, or platform-specific modules
  5. Custom JDK runtimesjlink to ship minimal container images (covered in Part 3)
  6. Migration safety — detect split packages and missing modules before production

Read the Full Article

Discover much more in Part 1: Fundamentals on blog.9mac.dev:

  • Project Jigsaw history and adoption data for 2026
  • Seven-module Library project walkthrough with architecture diagram
  • Full module-info.java reference for every directive
  • Qualified exports and the "friend modules" pattern
  • open module vs per-package opens
  • Module path, modular JARs, and --show-module-resolution
  • Building and running from the command line
  • Complete expected output from gradle :library-app:run

GitHub Repository

All code examples are ready to clone and run:

git clone https://github.com/dawid-swist/blog-9mac-dev-code.git
cd blog-9mac-dev-code/blog-post-examples/java/2026-07-13-java9-module-system

gradle build
gradle :blog-post-examples:java:2026-07-13-java9-module-system:library-app:run
Enter fullscreen mode Exit fullscreen mode

Requires Java 25+ and Gradle 9.


Next in the series: Part 2 — Deep Dive (unnamed and automatic modules, JDK internal encapsulation, and command-line escape hatches)

Top comments (0)