DEV Community

Cover image for 🧭 Java Modules: From Zero to Mastery
AK
AK

Posted on

🧭 Java Modules: From Zero to Mastery

πŸ“– Chapter 1: Introduction

1. Overview 🌍

πŸ’‘ Before diving into syntax and declarations β€” pause: Why did Java need modules at all? What problem does JPMS solve that packages couldn’t?

Java 9 introduced a groundbreaking architectural shift: the Java Platform Module System (JPMS) β€” more commonly called Modules. πŸ“¦βž‘οΈπŸ§±

This isn’t just β€œpackages, but bigger.” It’s a new level of encapsulation and dependency management, sitting above packages and below JARs β€” bringing true modularity to the Java platform for the first time.

πŸ”‘ Core Ideas

Concept Before JPMS With JPMS
Encapsulation public = visible to all public + module export = visible only to allowed modules
Dependencies Implicit (classpath chaos πŸŒͺ️) Explicit (requires declarations βœ…)
JRE Size Monolithic rt.jar (~70MB+) Slim, custom runtimes via jlink πŸ› οΈ

πŸ€” Reflect: Have you ever faced β€œJar Hell”? Classpath conflicts? Accidental API exposure? JPMS was born from these pains.

🎯 What We’ll Do

In this tutorial, we’ll:

  • βœ… Understand module declarations (module-info.java)
  • βœ… Explore requires, exports, opens, uses, provides
  • βœ… Build a small, modular project step-by-step β€” learning by doing
  • βœ… See how jdeps, jlink, and jmod empower modular deployment

πŸš€ Our goal: Not just know modules β€” but think in modules.

Let’s begin our journey β€” from the monolith 🏰 to the modular world 🧩.


πŸ“¦ Chapter 2: What Is a Module?

πŸ” Before asking β€œHow do I use modules?” β€” let’s ask: What problem does a β€œmodule” *actually solve? What makes it more than just a folder of packages?*

A module is not just a bundle β€” it’s a contract.

It groups related packages + resources, andβ€”cruciallyβ€”declares what it offers, what it needs, and what it hides.

Think of it as:

πŸ“¦ A package of packages β€” plus intentionality.

Let’s unpack its anatomy 🧬:

2.1 Packages β€” The Familiar Foundation 🧱

βœ… Still the same com.example.util you know and love.

βœ… Still organize code, avoid naming collisions.

➑️ But now:

Even if a class is public, it’s not accessible outside the module unless its package is explicitly exported.

πŸ€” Pause: Why might hiding public APIs be a good thing? (Hint: stability, security, maintainability.)

2.2 Resources β€” No More β€œWhere’s That Config?” πŸ–ΌοΈβš™οΈ

Before JPMS: resources scattered in src/main/resources, global classpath β€” hard to trace ownership.

With modules:

  • Resources live alongside the code that uses them.
  • Each module ships its own assets (images, configs, i18n files).
  • No more accidental overwrites or β€œwho owns logback.xml?” debates.

➑️ Result: Self-contained, relocatable units. πŸŽ’

2.3 The Heart: module-info.java β€οΈπŸ“œ

This tiny file is where intention becomes reality. It declares:

Directive Purpose Default?
module my.app { Name β€” e.g., com.baeldung.core (Reverse-DNS βœ…) or my.app (project-style βœ…) β€”
requires java.sql; Dependencies β€” explicit, compile-time enforced ❌ (NoClassDefFoundError if missing)
exports com.myapp.api; Public API β€” only exported packages are visible externally ❌ (all packages private by default!)
opens com.myapp.internal; Reflection access β€” allows frameworks (e.g., Spring, Hibernate) to access non-public members ❌ (reflection blocked by default!)
provides MyService with MyServiceImpl; Service provider β€” contributes an implementation β€”
uses MyService; Service consumer β€” declares intent to use a service β€”

πŸ“ Naming tip: Use lower.case.with.dots β€” no dashes, no uppercase.

πŸ›‘ Critical insight: Encapsulation is now strict by default. Freedom requires explicit permission.

2.4 Module Types β€” Who’s on the Path? 🧭

Not all modules are created equal. The JVM sees four kinds:

Type How It’s Loaded Access Privileges Example
System Modules πŸ–₯️ Built into JDK (java.base, java.sql, etc.) Highly restricted; java.base is the root java.base, jdk.jshell
Application Modules 🧩 module-info.java β†’ module-info.class in JAR Full JPMS rules apply Our own code βœ…
Automatic Modules πŸ€– Plain JAR on --module-path (no module-info) Reads all modules ⚠️ (loose coupling) Legacy commons-lang3-3.12.jar
Unnamed Module πŸ•³οΈ On --class-path (legacy mode) Reads all modules, but exports nothing Old-school classpath apps

πŸ’‘ Reflect: Why might automatic modules be a β€œbridge,” not a destination? What risks do they introduce?

2.5 Distribution β€” One Module, One JAR πŸ“¦πŸ“¦

πŸ”§ Rule: One module = one JAR.

You can distribute as:

  • βœ… A standard modular JAR (META-INF/versions/9/module-info.class inside)
  • βœ… An β€œexploded” directory (e.g., during dev/testing)

πŸ“¦ For multi-module projects (e.g., app + core + utils):

β†’ Each module builds to its own JAR β†’ assembled together at runtime.

⚠️ Gotcha: Trying to cram two modules into one JAR? The JVM will reject it. 🚫

🧩 Summary: A Module Is…

A named, self-describing, encapsulated unit of code + resources

β€” with explicit dependencies, APIs, and boundaries.

It turns implicit assumptions into explicit contracts.

Ready to see it in action? πŸ› οΈ

➑️ In the next chapter, we’ll build our first module β€” from module-info.java to runtime.


🧱 Chapter 3: Default Modules β€” The JDK’s Modular Heart

πŸ’‘ Before writing your first module-info.java, pause: What does the JDK itself look like now? If *it is modular, what can we learn from its design?*

With Java 9+, the JDK itself was refactored into modules β€” no more monolithic rt.jar! πŸͺ“βž‘️🧩

This wasn’t just internal cleanup: it enables custom runtimes, stronger security, and faster startup.

Let’s explore this new landscape.

πŸ” Discovering System Modules

Run this in your terminal:

java --list-modules
Enter fullscreen mode Exit fullscreen mode

You’ll see dozens of entries like:

java.base@17
java.sql@17
java.xml@17
jdk.jconsole@17
javafx.controls@17   # if installed
Enter fullscreen mode Exit fullscreen mode

Each is a named, self-contained system module β€” compiled, versioned, and interdependent.

🧭 The Four Module Families

The JDK’s modules fall into four logical groups β€” each with a purpose and visibility boundary:

Prefix Purpose Examples Key Insight
java.* 🌐 Java SE Platform API β€” what you’re allowed to depend on in portable apps java.base, java.sql, java.xml, java.desktop java.base is the root β€” every module implicitly requires it 🌱
javafx.* πŸ–ΌοΈ JavaFX UI Toolkit (modularized separately since Java 11) javafx.controls, javafx.fxml Not part of SE β€” must be added explicitly (e.g., via SDK or Maven)
jdk.* βš™οΈ JDK Tools & Implementation Details β€” internal to the JDK jdk.jshell, jdk.compiler, jdk.jdi ❗ Avoid depending on these β€” no stability guarantees!
oracle.* πŸ›‘οΈ Oracle-Specific Extensions (e.g., commercial features) oracle.jdbc, oracle.security Vendor-specific β€” not portable across JVMs (OpenJDK won’t have these)

πŸ€” Reflect: Why separate `java. (public spec) from jdk.` (implementation)? How does this help long-term evolution and security?

🌐 The Module Graph β€” Dependencies in Action

Every module declares its dependencies. For example:

  • java.sql β†’ requires java.logging, requires java.xml, requires transitive java.base
  • java.desktop β†’ requires java.prefs, requires transitive java.datatransfer

You can visualize dependencies with:

java --list-modules --verbose   # or
jdeps --list-deps $(java --list-modules | grep java.base | cut -d@ -f1)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ This is exactly the same model you’ll use for your own modules β€” just scaled up.

The JDK is the ultimate case study in modular design. πŸ“š

βœ… Key Takeaways

  • βœ… The JDK is now a collection of modules, not a single JAR.
  • βœ… java.base is the universal foundation β€” minimal, essential, and stable.
  • βœ… Separation of concerns is enforced: public API (java.*) vs. internal tools (jdk.*).
  • βœ… Your app’s modules will sit alongside these β€” depending only on what they truly need.

πŸš€ Next: Let’s create our own module β€” and see how it integrates with java.base, java.sql, or others.


πŸ“œ Chapter 4: Module Declarations β€” The Contract of Intent

πŸ€” Before writing module-info.java: What makes a good contract? Clarity? Minimalism? Explicit boundaries? Modules force us to negotiate these intentionally.

Every module starts with one file:

πŸ“ module-info.java β€” at the root of your source tree (side-by-side with your top-level package).

This is your module’s manifesto β€” declaring what it is, what it needs, and what it offers.

module my.app { 
    // directives go here β€” all optional, but rarely *all* omitted!
}
Enter fullscreen mode Exit fullscreen mode

Let’s explore each directive β€” not just what it does, but when (and why) to use it.

πŸ”— 4.1 requires β€” The Baseline Dependency

requires java.sql;
Enter fullscreen mode Exit fullscreen mode

βœ… What: Declares a mandatory compile-and-runtime dependency.

βœ… Effect: Public types from java.sql (e.g., Connection, Statement) are now usable in your module.

⚠️ Note: If java.sql isn’t on the module path? β†’ compile error.

πŸ’‘ Ask: Is this dependency truly required for my module to function? If yes β†’ requires.

βš–οΈ 4.2 requires static β€” Optional at Runtime

requires static org.slf4j;
Enter fullscreen mode Exit fullscreen mode

βœ… Compile-time only.

βœ… Your code can reference SLF4J types β€” but if SLF4J isn’t present at runtime? β†’ no error (assuming you guard usage with Class.forName() or DI).

🎯 Use case: Optional integrations (logging, metrics, debug tooling).

πŸ€” Reflect: How does this help library authors avoid β€œdependency bloat” for consumers?

πŸŒ‰ 4.3 requires transitive β€” β€œBring My Friends”

requires transitive com.fasterxml.jackson.databind;
Enter fullscreen mode Exit fullscreen mode

βœ… If Module A requires your module β†’ A automatically reads jackson.databind, too.

βœ… Critical for API libraries where your public types return or accept types from a dependency.

🚨 Anti-pattern: Overusing transitive β†’ unnecessary coupling.

βœ… Best practice: Only for dependencies whose types leak into your public API.

πŸšͺ 4.4 exports β€” Opening the Gate (Selectively)

exports com.myapp.api;
Enter fullscreen mode Exit fullscreen mode

βœ… Makes public types in com.myapp.api accessible to all modules that require yours.

πŸ”’ Default: All packages are module-private β€” even public classes are hidden.

πŸ’‘ Rule of thumb: Export only your *stable, intended public API β€” not internals.*

🎯 4.5 exports … to β€” Invite-Only Access

exports com.myapp.internal to com.myapp.test, com.myapp.debug;
Enter fullscreen mode Exit fullscreen mode

βœ… Grants access only to specified modules.

πŸ›‘οΈ Use for:

  • Test-only APIs
  • Friend modules (e.g., core β†’ cli and gui, but not public consumers)

🀫 Security win: Your β€œinternal” stays internal β€” except for trusted allies.

πŸ”Œ 4.6 uses β€” β€œI Consume This Service”

uses javax.persistence.PersistenceProvider;
Enter fullscreen mode Exit fullscreen mode

βœ… Declares: β€œI will look up implementations of this service interface at runtime (via ServiceLoader)”.

βœ… Does not imply requires β€” the provider module supplies the interface + impl.

🧩 Key insight: Decouples consumers from providers. Your module only needs the interface, not the impl.

🎁 4.7 provides … with β€” β€œI Am a Service Provider”

provides javax.persistence.spi.PersistenceProvider 
    with com.myapp.MyPersistenceProvider;
Enter fullscreen mode Exit fullscreen mode

βœ… Registers your class as an implementation of a service.

βœ… At runtime, ServiceLoader.load(PersistenceProvider.class) will find it β€” if your module is on the module path.

πŸ”„ Pattern: Clean separation of API (in one module) and implementations (in others).

πŸͺž 4.8 open module β€” Full Reflection (Use Sparingly!)

open module my.app {}
Enter fullscreen mode Exit fullscreen mode

βœ… Grants all modules full reflective access to all packages (including private members).

⚠️ Only use for:

  • Legacy frameworks that require deep reflection (e.g., older Hibernate, Spring versions)
  • Quick prototyping β€” not production!

🚫 Avoid if possible β€” breaks encapsulation.

πŸ” 4.9 opens β€” Reflect on This Package

opens com.myapp.config;
Enter fullscreen mode Exit fullscreen mode

βœ… Grants all modules reflective access to one package.

βœ… Safer than open module β€” but still broad.

πŸ’‘ Use when a specific package needs injection/mapping (e.g., config beans).

🎯 4.10 opens … to β€” Reflection, by Invitation Only

opens com.myapp.domain to spring.core, hibernate.core;
Enter fullscreen mode Exit fullscreen mode

βœ… Grants reflective access only to listed modules.

βœ… Best practice for modern apps: explicit, minimal, secure.

πŸ† Gold standard for production modules needing framework integration.

🧩 Putting It All Together β€” A Realistic Example

module com.baeldung.app {
    requires java.sql;
    requires static org.slf4j;
    requires transitive com.fasterxml.jackson.core;

    exports com.baeldung.api;
    exports com.baeldung.spi to com.baeldung.impl;

    opens com.baeldung.domain to spring.core;

    uses com.baeldung.spi.Plugin;
    provides com.baeldung.spi.Plugin with com.baeldung.plugins.DefaultPlugin;
}
Enter fullscreen mode Exit fullscreen mode

βœ… Minimal dependencies

βœ… Clear API boundaries

βœ… Secure reflection

βœ… Service-based extensibility

πŸ“Œ Pro Tips

  • πŸ“ Keep module-info.java clean: Group related directives (e.g., all requires, then exports, etc.).
  • πŸ§ͺ Test early: Use jdeps to analyze dependencies; java --describe-module to inspect at runtime.
  • πŸ›‘ Avoid: exports/opens to ALL-UNNAMED β€” it weakens modularity.

βš™οΈ Chapter 5: Command-Line Mastery β€” Beyond javac & java

πŸ€” If modules are declared in module-info.java, why do we need CLI flags? When does runtime flexibility outweigh compile-time rigidity?

While Maven/Gradle handle most build plumbing, CLI options give you surgical control β€” for:

  • 🐞 Debugging module resolution
  • πŸ§ͺ Patching or overriding in development
  • πŸ› οΈ Running legacy code in modular JVMs
  • πŸ” Understanding how the module system really works

Let’s demystify the key flags β€” with why, when, and how.

🧭 Essential Module Path Flags

Flag Purpose Example When to Use
--module-path (or -p) πŸ”— Where to find modules (replaces CLASSPATH for modular code) java -p mods:lib -m my.app/com.myapp.Main βœ… Always β€” for any modular app
--class-path (or -cp) πŸ•³οΈ For non-modular (unnamed module) code only java -cp legacy.jar com.LegacyApp ⚠️ Avoid mixing with -p unless bridging old/new

πŸ’‘ Pro tip: -p mods = mods/ contains JARs (or exploded dirs) with module-info.class.

πŸ› οΈ Runtime Overrides β€” β€œDynamic Directives”

These let you patch module behavior without recompiling β€” powerful, but use with care.

Flag Replaces Example Why?
--add-reads <module>=<other> requires (but runtime-only) --add-reads my.app=java.sql πŸ”§ Fix missing requires in 3rd-party JARs (e.g., automatic modules)
--add-exports <module>/<pkg>=<target> exports … to --add-exports java.base/sun.nio.ch=my.app 🚨 Access internal JDK APIs (e.g., for performance hacks β€” not recommended for prod!)
--add-opens <module>/<pkg>=<target> opens … to --add-opens java.base/java.lang=my.app πŸ§ͺ Allow reflection into JDK internals (e.g., for testing, mocking, or legacy frameworks)
--patch-module <module>=<path> Replace/extend a module --patch-module java.base=patches/ πŸ› οΈ Hotfix JDK bugs during dev; inject test doubles

⚠️ Warning: Overuse breaks encapsulation β€” these are escape hatches, not design features.

πŸ€” Reflect: How might --add-opens help migrate a Spring 4 app to Java 17? What trade-offs does it introduce?

πŸ“‹ Inspection & Control

Flag Purpose Example Insight
--list-modules πŸ“œ Show all resolved modules (name + version) `java --list-modules \ grep java.`
--describe-module <name> πŸ” Deep-dive into a module’s structure java --describe-module java.sql View exports, requires, services β€” like module-info.java at runtime!
--add-modules <mod1>,<mod2> βž• Explicitly resolve extra modules --add-modules java.xml.bind (in Java 9–10) Needed for modules not required by your app but used indirectly (e.g., via reflection)

πŸ›‘οΈ Strong Encapsulation β€” The --illegal-access Lever

Java 9+ blocks illegal reflective access by default β€” but offers a grace period:

Mode Effect CLI Reality Check
permit (default ≀ Java 16) 🟑 Warn once at startup --illegal-access=permit β€œWorks, but noisy” β€” deprecated in Java 17+
warn 🟠 Warn every time illegal access occurs --illegal-access=warn Find hidden reflection issues
deny (default β‰₯ Java 17) πŸ”΄ Fail fast on illegal access --illegal-access=deny βœ… Production best practice

πŸ’‘ In Java 17+, --illegal-access is ignored β€” illegal access is always denied.

πŸ› οΈ Fix properly with --add-opens or refactor.

πŸ§ͺ Real-World Example: Running a β€œBroken” Modular App

Imagine my-app.jar forgets to requires java.sql β€” but uses JDBC.

❌ Fails with:

java.lang.module.ResolutionException: Module my.app does not read module java.sql

βœ… Fix temporarily via CLI:

java \
  --module-path mods \
  --add-reads my.app=java.sql \
  -m my.app/com.myapp.Main
Enter fullscreen mode Exit fullscreen mode

β†’ Works! But now you know: go fix module-info.java πŸ› οΈ.

βœ… Key Principles

  • Compile-time declarations > runtime overrides β€” use CLI for debugging, not design.
  • Least privilege: Prefer --add-opens … to my.module over global opens.
  • Know your defaults: --illegal-access=deny is the new normal in modern Java.

πŸ” Chapter 6: Visibility & Reflection β€” The New Rules of Access

πŸ€” Before Java 9: β€œIf it’s loaded, I can reflect on it.”

After Java 9: β€œIf it’s not explicitly opened β€” no reflection, not even with setAccessible(true).”

Why did this change? What does β€œsecure by default” really mean?

Strong encapsulation isn’t just about hiding code β€” it’s about predictability, security, and evolvability.

But yes β€” it does break reflection-heavy frameworks. πŸ˜… Let’s navigate this wisely.

🧱 The New Visibility Hierarchy

In Java 9+, accessibility is a two-layer gate:

Layer Gatekeeper What It Controls
1. Module Readability requires / --add-reads Can Module A see Module B at all?
2. Package Accessibility exports / opens / CLI flags Can Module A access types or members in Module B’s packages?

➑️ Both must be satisfied β€” even for reflection.

πŸ” What’s Really Accessible? (By Default)

Member Type Normal Access (new, method call) Reflection (getDeclaredField() + setAccessible(true))
public in exported package βœ… Yes βœ… Yes
public in non-exported package ❌ No ❌ No
private/protected/package-private in exported package ❌ No ❌ No β†’ InaccessibleObjectException!
Any member in opened package ❌ (compile) / βœ… (runtime via reflection) βœ… Yes β€” if module opened it to you

πŸ’₯ Critical: setAccessible(true) does not bypass module encapsulation.

It only bypasses Java language access checks β€” not module system checks.

πŸ› οΈ How to Grant Reflection Access (The Right Way)

βœ… Preferred: Declare It in module-info.java

Directive Scope When to Use
open module my.module { } Entire module βœ… Quick dev/test; framework-heavy apps (e.g., older Spring)
opens com.my.pkg; One package β†’ all modules ⚠️ Rare β€” too permissive
opens com.my.pkg to spring.core, junit; One package β†’ specific modules πŸ† Production best practice
// module-info.java β€” clean, intentional, auditable
module com.baeldung.app {
    opens com.baeldung.domain to spring.core, hibernate.core;
    opens com.baeldung.config to spring.core;
}
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ Escape Hatch: CLI Overrides (When You Can’t Change the Module)

If you’re using a 3rd-party library that’s not modular (or poorly modularized):

java \
  --module-path mods \
  --add-opens java.base/java.lang=com.example.app \
  --add-opens java.desktop/sun.awt=com.example.app \
  -m com.example.app/com.example.Main
Enter fullscreen mode Exit fullscreen mode

🎯 Use cases:

  • Running legacy frameworks on Java 17+
  • Patching missing opens in automatic modules
  • CI/CD environments where you control JVM args

⚠️ Limitations:

  • Requires control over launch command (❌ not possible in some cloud/serverless envs)
  • Doesn’t help if the framework itself doesn’t use setAccessible(true) properly

πŸ§ͺ Real-World Examples

πŸ”§ Spring Boot (Pre-3.0)

Many beans use reflection on private fields.

βœ… Fix:

opens com.myapp.domain to spring.core, spring.beans;
Enter fullscreen mode Exit fullscreen mode

Or (temporarily):

--add-opens com.myapp/com.myapp.domain=spring.core
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ JUnit 5

Uses reflection to instantiate/test private methods.

βœ… Fix:

opens com.myapp to org.junit.platform.commons;
Enter fullscreen mode Exit fullscreen mode

(Or use @ExtendWith and public test methods β€” even better! 🌟)

🚫 Anti-Patterns to Avoid

What Why It’s Bad
--add-opens ALL-UNNAMED=ALL-UNNAMED ❌ Defeats modularity; insecure
Exporting internal packages just for reflection ❌ Confuses API contract (exports β‰  opens!)
Ignoring InaccessibleObjectException ❌ Hides design debt β€” will break in future JDKs

πŸ’‘ Pro Tips for Library Authors

  1. Separate API from implementation:
    • exports your public interfaces
    • opens only internal packages to your own test module
  2. Prefer constructor/setter injection over field injection β€” reduces reflection needs.
  3. Document reflection requirements in your module README: > ℹ️ This module requires --add-opens com.lib/internal=your.app if used with Framework X.

πŸ”„ The Bigger Picture

This shift isn’t about making life harder β€” it’s about:

  • πŸ›‘οΈ Preventing accidental coupling to internals (e.g., sun.misc.Unsafe)
  • πŸš€ Enabling JVM optimizations (e.g., ahead-of-time compilation, smaller images)
  • 🌱 Allowing JDK teams to evolve internal APIs safely

As Brian Goetz said:

β€œModules don’t take away reflection β€” they take away *surprise reflection.”*

🧩 Chapter 7: Putting It All Together β€” A Modular Hello World

πŸ€” Now that we know the rules β€” can we *feel modularity? Let’s build, break, and fix β€” with nothing but javac, java, and intention.*

We’ll create a two-module app β€” then extend it with services β€” all from the command line.

No Maven. No Gradle. Just pure JPMS. πŸ–₯️✨

πŸ“‚ 7.1 Project Structure β€” Modular by Design

Let’s build a clean, scalable layout:

mkdir -p module-project/simple-modules
cd module-project
Enter fullscreen mode Exit fullscreen mode

πŸ“ Final structure:

module-project/
β”œβ”€β”€ compile-simple-modules.sh   # ← build script
β”œβ”€β”€ run-simple-module-app.sh    # ← run script
└── simple-modules/
    β”œβ”€β”€ hello.modules/          # ← Library module
    β”‚   β”œβ”€β”€ module-info.java
    β”‚   └── com/baeldung/modules/hello/
    β”‚       β”œβ”€β”€ HelloModules.java
    β”‚       └── HelloInterface.java   # ← added later
    β”‚
    └── main.app/               # ← Application module
        β”œβ”€β”€ module-info.java
        └── com/baeldung/modules/main/
            └── MainApp.java
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Why this layout?

  • simple-modules/ isolates all modules β€” easy to add more (util, config, etc.)
  • Flat sibling structure β†’ clean --module-source-path

πŸ“¦ 7.2 Module 1: hello.modules β€” The API Provider

βœ… Step 1: Create the class

simple-modules/hello.modules/com/baeldung/modules/hello/HelloModules.java

package com.baeldung.modules.hello;

public class HelloModules {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 2: Declare the module

simple-modules/hello.modules/module-info.java

module hello.modules {
    exports com.baeldung.modules.hello;
}
Enter fullscreen mode Exit fullscreen mode

πŸ€” Reflect: What happens if we omit exports? Try it β€” see the compile error!

πŸ”’ Encapsulation in action: Without exports, HelloModules is invisible β€” even though it’s public.

πŸš€ 7.3 Module 2: main.app β€” The Consumer

βœ… Step 1: Declare dependency

simple-modules/main.app/module-info.java

module main.app {
    requires hello.modules;  // ← explicit, compile-time enforced
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 2: Use the API

simple-modules/main.app/com/baeldung/modules/main/MainApp.java

package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloModules;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();  // ← works because package is exported!
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Note: No import static needed β€” doSomething() is static, not a service (yet!).

πŸ”¨ 7.4 Build Script β€” One Command to Rule Them All

compile-simple-modules.sh

#!/usr/bin/env bash
set -e  # exit on error

echo "πŸ” Compiling all modules..."
javac \
  -d outDir \
  --module-source-path simple-modules \
  $(find simple-modules -name "*.java")

echo "βœ… Modules built to: outDir/"
ls -R outDir
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ Key flags:

  • -d outDir β†’ output directory
  • --module-source-path simple-modules β†’ tells javac: β€œThis is a multi-module project”
  • $(find ...) β†’ auto-includes all .java files (no manual lists!)

πŸ› οΈ Run it:

chmod +x compile-simple-modules.sh
./compile-simple-modules.sh

βœ”οΈ Expect:

outDir/
β”œβ”€β”€ hello.modules/
β”‚   └── com/baeldung/modules/hello/HelloModules.class
└── main.app/
    └── com/baeldung/modules/main/MainApp.class
Enter fullscreen mode Exit fullscreen mode

▢️ 7.5 Run It β€” The Moment of Truth!

run-simple-module-app.sh

#!/usr/bin/env bash
java \
  --module-path outDir \
  -m main.app/com.baeldung.modules.main.MainApp
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ --module-path outDir = where to find compiled modules

πŸ”‘ -m main.app/... = run MainApp.main() in module main.app

πŸš€ Run it:

chmod +x run-simple-module-app.sh
./run-simple-module-app.sh
Enter fullscreen mode Exit fullscreen mode

🎯 Expected output:

Hello, Modules!
Enter fullscreen mode Exit fullscreen mode

πŸŽ‰ Success! You’ve built your first modular app.

πŸ€” What if you swap requires hello.modules β†’ requires static hello.modules and remove the call? Does it still compile? Run?

πŸ”Œ 7.6 Level Up: Services with provides … with & uses

Let’s replace static calls with pluggable services β€” the real power of JPMS.

βœ… Step 1: Define the service interface

simple-modules/hello.modules/com/baeldung/modules/hello/HelloInterface.java

package com.baeldung.modules.hello;

public interface HelloInterface {
    void sayHello();
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 2: Implement it

Update HelloModules.java:

public class HelloModules implements HelloInterface {  // ← now an impl
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }

    @Override
    public void sayHello() {
        System.out.println("Hello from Service!");
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 3: Declare the service provider

Update hello.modules/module-info.java:

module hello.modules {
    exports com.baeldung.modules.hello;

    provides com.baeldung.modules.hello.HelloInterface  // ← service contract
        with com.baeldung.modules.hello.HelloModules;     // ← implementation
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 4: Declare the consumer

Update main.app/module-info.java:

module main.app {
    requires hello.modules;
    uses com.baeldung.modules.hello.HelloInterface;  // ← "I will load this service"
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 5: Load the service

Update MainApp.java:

package com.baeldung.modules.main;

import com.baeldung.modules.hello.HelloInterface;
import java.util.ServiceLoader;

public class MainApp {
    public static void main(String[] args) {
        // Static call (still works)
        com.baeldung.modules.hello.HelloModules.doSomething();

        // Service-based call (new!)
        ServiceLoader<HelloInterface> loader = ServiceLoader.load(HelloInterface.class);
        HelloInterface service = loader.findFirst()
            .orElseThrow(() -> new RuntimeException("No HelloInterface found!"));
        service.sayHello();
    }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Step 6: Recompile & Run

./compile-simple-modules.sh
./run-simple-module-app.sh
Enter fullscreen mode Exit fullscreen mode

🎯 New output:

Hello, Modules!
Hello from Service!
Enter fullscreen mode Exit fullscreen mode

🎯 Why this matters:

  • Your app no longer depends on HelloModules β€” only on HelloInterface.
  • Swap implementations without recompiling main.app β€” just drop in a new module!
  • Hide implementations in non-exported packages (e.g., com.baeldung.internal) β€” only the interface is public.

πŸ§ͺ Try It Yourself! (Mini Challenges)

Now that you’ve got the foundation β€” experiment!

  1. πŸšͺ Move HelloModules to com.baeldung.internal β€” can you still use it via service? (Hint: no exports needed!)
  2. 🧩 Add a second implementation (HelloImpl2) β€” what does ServiceLoader return?
  3. πŸ”“ Add opens com.baeldung.modules.hello β€” can you reflect on private fields now?
  4. 🚫 Remove uses β€” does it still compile? Run? (Spoiler: compile βœ…, runtime ❌ if no impl found)

πŸŽ“ Key Takeaways

  • βœ… Modules = explicit contracts, not implicit assumptions.
  • βœ… exports β‰  opens β€” API vs. reflection access.
  • βœ… Services (provides/uses) enable loose coupling and runtime discovery.
  • βœ… CLI tools (javac, java) are your best friends for learning.

πŸ•³οΈ Chapter 8: The Unnamed Module β€” Java’s Backward-Compatibility Lifeline

πŸ€” If modules are so powerful β€” why does Java still allow code *outside the module system? What trade-offs did the designers make to avoid breaking the world?*

The unnamed module is not a β€œmodule” in the formal sense β€” it’s a compatibility construct:

πŸ“¦ All code on the --class-path (not --module-path) lives here β€” as one big, flat, β€œlegacy” module.

It has special privileges β€” and limitations β€” designed to keep pre-Java 9 code running while encouraging migration.

🧩 What Is the Unnamed Module?

Property Unnamed Module Named Module
How created Put JAR/class on --class-path Put JAR/dir with module-info.class on --module-path
Name null (no name) e.g., com.baeldung.app
Reads βœ… All other modules (system + named + automatic) ❌ Only modules it requires
Exports ❌ Exports nothing β†’ all packages are module-private βœ… Only packages explicitly exports
Opens ❌ Opens nothing for reflection (unless CLI overrides used) βœ… Controlled via opens/open module

πŸ’‘ Key insight:

The unnamed module is omnivorous (it can use anything) but mute (it offers nothing to others).

β†’ Great for running old apps β€” poor for building modular ones.

πŸ”Œ Why Add Modules Explicitly? (--add-modules)

Even though the unnamed module reads all modules, some modules are *not resolved by default* β€” especially if they’re not required by anything in the root set.

🎯 Common Scenarios

Problem Cause Fix
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException java.xml.bind was removed from default root set in Java 9+ (later deleted in Java 11) --add-modules java.xml.bind (Java 9–10 only)
ServiceConfigurationError: No implementation for javax.persistence.spi.PersistenceProvider JPA impl (e.g., Hibernate) needs java.sql, but unnamed module app doesn’t pull it in --add-modules java.sql
ClassNotFoundException: com.sun.xml.internal.ws.spi.ProviderImpl Internal JDK service provider not resolved --add-modules jdk.xml.ws (if available)

⚠️ Note: In Java 11+, java.xml.bind, java.activation, etc., are gone β€” you must add them as dependencies (e.g., jakarta.xml.bind-api).

βš™οΈ How --add-modules Works

java --add-modules java.sql,java.xml -cp legacy-app.jar com.LegacyMain
Enter fullscreen mode Exit fullscreen mode
  • πŸ” Tells the JVM: β€œEven if no module explicitly requires these, include them in the module graph.”
  • βœ… Resolves the module + its transitive dependencies
  • βœ… Makes their exported packages available to the unnamed module (via its β€œread all” privilege)

πŸ€” Reflect: Why not just auto-resolve *all system modules?*

🎯 Answer: To keep minimal runtimes lean β€” unused modules aren’t loaded.

πŸ§ͺ Real-World Example: Running a Java 8 Spring App on Java 17

Your old spring-boot-1.5 app uses JAXB for REST β†’ fails on Java 17 with:

java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlRootElement

βœ… Solution 1 (Temporary): Add Jakarta EE API + CLI flag

<!-- pom.xml -->
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>4.0.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode
java \
  --add-modules ALL-SYSTEM \          # resolves *all* system modules
  --add-opens java.base/java.lang=ALL-UNNAMED \
  -jar legacy-app.jar
Enter fullscreen mode Exit fullscreen mode

βœ… Solution 2 (Better): Migrate to Jakarta XML Binding + Spring Boot 3

β†’ No CLI hacks needed β€” fully modular-friendly. 🌟

πŸ› οΈ In Build Tools

Maven (maven-compiler-plugin)

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.11.0</version>
  <configuration>
    <release>17</release>
    <compilerArgs>
      <arg>--add-modules</arg>
      <arg>java.sql,java.xml</arg>
    </compilerArgs>
  </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Gradle

tasks.withType(JavaCompile) {
    options.compilerArgs += ['--add-modules', 'java.sql,java.xml']
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Pro tip: Prefer adding only what you need β€” ALL-SYSTEM bloats the classpath and hides real dependencies.

🚫 Anti-Patterns to Avoid

What Why It’s Bad
--add-modules ALL-UNNAMED ❌ Invalid β€” ALL-UNNAMED isn’t a module name
Relying on --add-modules forever ❌ Masks design debt β€” migrate to named modules!
Using removed modules (e.g., java.xml.bind in Java 11+) ❌ Won’t work β€” replace with Jakarta EE APIs

πŸŒ‰ The Bridge Forward

The unnamed module is a temporary harbor, not a destination.

Use --add-modules to:

  • 🚒 Migrate incrementally (run old app β†’ modularize one module at a time)
  • πŸ§ͺ Diagnose missing dependencies (jdeps --print-module-deps your-app.jar)
  • πŸ“Š Audit your legacy code before full modularization

As the JDK evolves, fewer modules will be β€œmissing by default” β€” because fewer apps will need them.

You’re not just fixing a runtime error β€” you’re future-proofing your code. πŸ›‘οΈ


🏁 Chapter 9: Conclusion β€” You’ve Crossed the Modular Threshold

πŸ€” Look back: How has your understanding of β€œencapsulation” changed since Chapter 1? Was it just about private fields β€” or something deeper?

You’ve done it.

You’ve moved from implicit assumptions to explicit contracts.

From classpath chaos πŸŒͺ️ to intentional architecture 🧩.

From β€œit works (for now)” to β€œit’s designed to evolve.”

Let’s recap the journey:

πŸ“š Chapter 🎯 Core Insight
1. Overview Modules are not packages 2.0 β€” they’re a new layer of design intention.
2. What’s a Module? A module = packages + resources + module-info.java β€” a self-describing unit.
3. Default Modules Even the JDK practices what it preaches β€” modularity starts at the top. πŸ–₯️
4. Module Declarations requires, exports, opens, provides… each directive is a promise you make.
5. Command Line The JVM speaks modular β€” learn its language to debug, optimize, and understand.
6. Visibility Strong encapsulation isn’t restrictive β€” it’s liberating (once you adapt). πŸ”
7. Hands-On Theory becomes real when you type javac --module-source-path and see it work. πŸ› οΈ
8. Unnamed Module Backward compatibility is a bridge β€” not a destination. Walk across with care. πŸŒ‰

🌱 Where to Go From Here

You now hold the keys to:

βœ… Build modular libraries β€” with clean APIs, secure internals, and service extensibility.

βœ… Diagnose migration issues β€” using jdeps, --describe-module, and --add-modules.

βœ… Prepare for the future β€” where custom runtimes (jlink) and native images (GraalVM) are the norm.

πŸ”œ Next Steps (If You’re Curious…)

Path What You’ll Explore
πŸ“¦ Multi-Module Builds Maven/Gradle modular projects β€” moditect, module path vs. classpath
πŸ”— jlink: Custom Runtimes Strip the JDK down to only what your app needs β€” 50MB β†’ 20MB!
☁️ Modular Microservices How modules fit (or don’t) in containerized, cloud-native worlds
⚑ GraalVM Native Image Can modular apps be compiled to native? (Spoiler: Yes β€” with care!)

πŸ™ Final Thought

β€œModules don’t make Java harder β€” they make *bad design harder to ignore.”*

The module system rewards clarity, foresight, and respect for boundaries β€” between your code, your dependencies, and the platform itself.

You didn’t just learn syntax.

You’ve begun thinking like a modular architect. πŸ—οΈ


Top comments (0)