12.1 Module Architecture, `module-info.java`, and Module Paths
Key Takeaways
The Java Platform Module System (JPMS) solves 'JAR hell' and classpath fragility by introducing reliable configuration and strong encapsulation at the JVM and compiler levels.
The module declaration file, module-info.java, must reside at the root of the module source hierarchy and compiles into module-info.class at the root of the output directory or JAR.
The module-path (--module-path or -p) locates modular artifacts and validates dependencies at startup, contrasting with the linear, unencapsulated class search of the classpath.
Compilation and packaging commands (javac and jar) require explicit module flags (--module-source-path, -d, --module, --main-class) to build and run modular binaries.
JPMS strictly forbids circular dependencies among modules at compile time and link time, aborting immediately if cycles are detected.
Module Architecture, module-info.java, and Module Paths
The Java Platform Module System (JPMS), introduced in Java 9 via Project Jigsaw and refined through Java 17 LTS, represents the most significant architectural evolution of the Java runtime environment since its inception. Prior to modularization, Java applications suffered from fundamental scalability and security liabilities known collectively as "JAR hell." JPMS replaces implicit runtime classloading assumptions with an explicit, compiler-enforced contract of module boundaries.
Motivation and Core Architectural Goals
For over two decades, Java applications relied exclusively on the classpath. The classpath model suffered from three primary architectural flaws that JPMS directly resolves:
- Fragile, Linear Class Loading ("Classpath Hell"):
When locating classes, the JVM scans classpath entries sequentially from left to right. If two JAR files contain a class with the identical fully qualified name (e.g., due to conflicting library versions), whichever JAR appears first on the classpath satisfies the classloader, silently shadowing the second. Furthermore, missing dependencies are not detected at application startup; they manifest as runtime
ClassNotFoundExceptionorNoClassDefFoundErrorinstances hours or days after deployment when an unreferenced code path executes. - Monolithic JDK Runtime:
Before Java 9, the entire Java runtime library was packaged into massive, monolithic binaries—chiefly
rt.jar(runtime) andtools.jar—exceeding 60 megabytes. Even trivial command-line utilities or microservices were forced to load or bundle the entire Java standard library, including desktop GUI stacks (java.desktop) and CORBA legacy subsystems. JPMS dismantledrt.jar, partitioning the JDK into dozens of distinct modules (such asjava.base,java.sql,java.logging, andjava.net.http). - Lack of True Encapsulation:
In the legacy model, access modifiers were bounded only by package scope. Any class marked
publicwas accessible to every other class on the classpath. Internal JDK implementation classes (such assun.misc.Unsafeor internal security adapters) could not be hidden from user code, creating fragile couplings that hindered JVM evolution.
The Two Pillars: Reliable Configuration and Strong Encapsulation
JPMS establishes two foundational pillars:
- Reliable Configuration: Explicit dependency graphs declared in module descriptors are verified by the compiler and at JVM launch before
main()executes. If any required module is absent, duplicate, or creates an illegal dependency, execution aborts immediately. - Strong Encapsulation: A public class within a module is no longer universally public. It is accessible outside its parent module only if its containing package is explicitly exported by that module, and the consuming module explicitly declares a readability requirement on it.
Anatomy of module-info.java
A module is a uniquely named, self-describing collection of code and data. The descriptor is defined in a special file named module-info.java.
Placement and Structure
The module-info.java file must reside in the root of the module's source folder hierarchy, directly alongside the top-level package directories (never inside a package subfolder). When compiled, it produces module-info.class, which must reside in the root of the compiled output directory and at the root of the resulting modular JAR file.
project-root/
└── src/
└── com.example.billing/
├── module-info.java
└── com/
└── example/
└── billing/
├── InvoiceService.java
└── internal/
└── TaxCalculator.java
The declaration uses the module contextual keyword followed by the module identifier and a body enclosed in braces:
module com.example.billing {
// Module directives: requires, exports, opens, uses, provides
}
Note
module, requires, exports, opens, uses, provides, with, and to are restricted contextual keywords. They act as keywords only within the context of a module-info.java file. In regular Java source code, you can freely use these terms as variable or method identifiers (e.g., int module = 42; or void exports() {}).
Module Naming Rules and Conventions
Module names follow reverse domain name notation (e.g., com.example.billing), mirroring standard package conventions. However, the compiler enforces strict identifier syntax:
- Module names consist of one or more identifiers separated by dots (
.). - Each identifier segment must be a valid Java identifier: it cannot start with a digit, cannot contain hyphens (
-), and cannot match reserved Java keywords. - Hyphens are strictly forbidden in module names in source declarations. A declaration like
module com.example-app { }produces a compilation error. (Tools automatically convert hyphens to dots when deriving automatic module names from JAR filenames).
Classpath vs. Module-Path
Understanding how the JVM resolves code requires distinguishing between the classpath and the module-path.
| Characteristic | Classpath (--class-path, -cp, -classpath) | Module-Path (--module-path, -p) |
|---|---|---|
| Mechanism | Sequential, flat search across directories and JARs | Graph-based resolution of named module artifacts |
| Encapsulation | None; all public classes are globally accessible | Strict; unexported packages are inaccessible outside the module |
| Dependency Check | Lazy, runtime discovery upon class referencing | Strict startup validation; aborts immediately if dependencies fail |
| Package Integrity | Permits split packages across different JAR files | Forbids split packages; identical package names abort loading |
| Descriptor File | Ignores module-info.class if present; treated as flat JAR | Requires or synthesizes module definitions (module-info.class) |
| Target Environment | Legacy code executing inside the Unnamed Module | Modern modular code executing inside Named Modules |
Modular applications are executed using the --module-path (or -p) option to supply the modules, and --module (or -m) to designate the initial module and main class:
java --module-path mods --module com.example.billing/com.example.billing.Main
# Equivalent short-form syntax:
java -p mods -m com.example.billing/com.example.billing.Main
Compiling, Packaging, and Running Modular Applications
Working with modular source code introduces specific command-line options for javac, jar, and java.
1. Compiling Modular Code
When compiling a single module, compile module-info.java alongside the module's source files:
javac -d out/com.example.billing \
src/com.example.billing/module-info.java \
src/com.example.billing/com/example/billing/*.java
When compiling multiple modules simultaneously, use the --module-source-path flag to point to the directory containing module subfolders:
javac -d out --module-source-path src $(find src -name "*.java")
The compiler automatically maps each subdirectory in src to its corresponding module output under out.
2. Packaging Modular JARs
A modular JAR is identical to a standard JAR except that it contains module-info.class at its root. You can record the application's main entry point directly into the modular JAR using the --main-class attribute:
jar --create --file=lib/billing.jar \
--main-class=com.example.billing.Main \
-C out/com.example.billing .
To inspect the compiled module descriptor inside a modular JAR:
jar --describe-module --file=lib/billing.jar
3. Executing Modular Applications
If the modular JAR was packaged with --main-class, the class name can be omitted from the -m option:
java -p lib -m com.example.billing
Prohibition of Circular Module Dependencies
One of JPMS's most uncompromising design constraints is the absolute prohibition of circular dependencies at compile time and link time.
[module A] ---> requires ---> [module B]
^ |
| |
+---------- requires ----------+
If module A requires module B, and module B requires module A (either directly or indirectly through a chain of dependencies), the compiler fails immediately:
error: cyclic dependence involving com.example.alpha and com.example.beta
Warning
The Circularity Trap
Circular dependencies cannot be circumvented using requires transitive, qualified exports, or runtime flags. The module system constructs a Directed Acyclic Graph (DAG) during layer resolution. If a cycle is present, the module layer cannot be instantiated. Developers must refactor common dependencies into a shared independent module or decouple implementations using JPMS service providers (uses and provides).
A developer organizes a multi-module project with the following directory structure:
src/
com.finance.core/
module-info.java
com/finance/core/Ledger.java
com.finance.reports/
module-info.java
com/finance/reports/ReportGen.java
Which command correctly compiles both modules and places the resulting compiled module classes into the out directory?
javac -d out --module-path src src/com.finance.core/module-info.java src/com.finance.reports/module-info.java
javac -d out --module-source-path src $(find src -name "*.java")
javac -d out --source-path src src/module-info.java
javac -d out/modules -cp src src/com.finance.*/*.java
Consider two modules with the following declarations:
// In module com.alpha/module-info.java
module com.alpha {
requires com.beta;
exports com.alpha.api;
}
// In module com.beta/module-info.java
module com.beta {
requires com.alpha;
exports com.beta.service;
}
What is the result when attempting to compile these two modules together?
Compilation fails immediately with an error indicating a cyclic dependence involving com.alpha and com.beta.
The compiler automatically merges both modules into a single synthetic module to break the circularity.
Both modules compile successfully because cyclic dependencies are resolved at runtime by the JVM.
Compilation succeeds only if com.alpha adds the transitive modifier: requires transitive com.beta;.
A modular application is packaged into lib/analytics.jar. The module descriptor defines the module name as com.data.analytics, and the JAR manifest includes the entry Main-Class: com.data.analytics.Engine. Which command correctly launches the modular application?
java --module-path lib --module com.data.analytics
java -p lib/analytics.jar -m com.data.analytics/Main
java --class-path lib/analytics.jar com.data.analytics.Engine
java --module-path lib com.data.analytics.Engine
Sections you finish are checked off in the contents.