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)