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.

Last updated: October 2026

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:

  1. 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 ClassNotFoundException or NoClassDefFoundError instances hours or days after deployment when an unreferenced code path executes.
  2. Monolithic JDK Runtime: Before Java 9, the entire Java runtime library was packaged into massive, monolithic binaries—chiefly rt.jar (runtime) and tools.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 dismantled rt.jar, partitioning the JDK into dozens of distinct modules (such as java.base, java.sql, java.logging, and java.net.http).
  3. Lack of True Encapsulation: In the legacy model, access modifiers were bounded only by package scope. Any class marked public was accessible to every other class on the classpath. Internal JDK implementation classes (such as sun.misc.Unsafe or 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.

CharacteristicClasspath (--class-path, -cp, -classpath)Module-Path (--module-path, -p)
MechanismSequential, flat search across directories and JARsGraph-based resolution of named module artifacts
EncapsulationNone; all public classes are globally accessibleStrict; unexported packages are inaccessible outside the module
Dependency CheckLazy, runtime discovery upon class referencingStrict startup validation; aborts immediately if dependencies fail
Package IntegrityPermits split packages across different JAR filesForbids split packages; identical package names abort loading
Descriptor FileIgnores module-info.class if present; treated as flat JARRequires or synthesizes module definitions (module-info.class)
Target EnvironmentLegacy code executing inside the Unnamed ModuleModern 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).

Test Your Knowledge

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?

A

javac -d out --module-path src src/com.finance.core/module-info.java src/com.finance.reports/module-info.java

B

javac -d out --module-source-path src $(find src -name "*.java")

C

javac -d out --source-path src src/module-info.java

D

javac -d out/modules -cp src src/com.finance.*/*.java

Test Your Knowledge

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?

A

Compilation fails immediately with an error indicating a cyclic dependence involving com.alpha and com.beta.

B

The compiler automatically merges both modules into a single synthetic module to break the circularity.

C

Both modules compile successfully because cyclic dependencies are resolved at runtime by the JVM.

D

Compilation succeeds only if com.alpha adds the transitive modifier: requires transitive com.beta;.

Test Your Knowledge

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?

A

java --module-path lib --module com.data.analytics

B

java -p lib/analytics.jar -m com.data.analytics/Main

C

java --class-path lib/analytics.jar com.data.analytics.Engine

D

java --module-path lib com.data.analytics.Engine

Sections you finish are checked off in the contents.