Key Concepts in Java Modules: Module, Module Descriptor, and Module Path Explained

Illustration for Key Concepts in Java Modules: Module, Module Descriptor, and Module Path Explained
By Last updated:

Many developers run into frustrating errors like “package not found” or “package is not visible” when moving from the classpath to the module path. What worked seamlessly in legacy Java suddenly breaks, leaving teams scrambling to understand why their code can no longer access certain classes.

This pain often arises from not fully grasping three foundational concepts: the module, the module descriptor (module-info.java), and the module path. These form the backbone of the Java Platform Module System (JPMS). Without mastering them, modularizing enterprise systems, microservices, or even simple libraries becomes error-prone.

Understanding these concepts matters in real-world scenarios:

  • Enterprise apps need clear boundaries between services.
  • Cloud-native deployments benefit from minimized runtimes with only necessary modules.
  • Legacy migration projects must balance classpath compatibility with modern modularization.

What is a Module?

A module is a named collection of code and resources (packages, classes, native libraries, configuration files) with clear boundaries. Unlike JARs on the classpath, modules explicitly state:

  • What they depend on (requires).
  • What they expose to others (exports).

Think of a module as an office department: it decides what internal documents remain private and what reports it shares with other departments.

Example

// File: module-info.java
module com.example.user {
    exports com.example.user.api;
}

Here:

  • The com.example.user module exists.
  • Only the user.api package is visible outside; internal packages remain hidden.

The Module Descriptor (module-info.java)

The module descriptor is a special file placed in the root of a module, named module-info.java. It describes:

  1. Dependenciesrequires other modules.
  2. Exports → Packages available for other modules.
  3. Services → Declared using uses and provides.
  4. Openness → Reflection access with open or opens.

Example

module com.example.payment {
    requires com.example.user;
    exports com.example.payment.api;
    uses com.example.payment.spi.PaymentProcessor;
    provides com.example.payment.spi.PaymentProcessor 
        with com.example.payment.impl.DefaultProcessor;
}

Explanation:

  • requires → Depends on the user module.
  • exports → Makes the payment.api package available externally.
  • uses → Declares a service interface.
  • provides ... with ... → Supplies an implementation.

Without module-info.java, a JAR is treated as an unnamed module (essentially classpath behavior).


The Module Path vs Classpath

The module path replaces the traditional classpath for modular applications.

Comparison

Feature Classpath Module Path
Scope All classes globally visible Controlled visibility via exports
Dependency Handling No explicit dependencies Explicit requires declaration
Error Detection Runtime (late) Compile-time (early, safer)
Encapsulation Weak, accidental access possible Strong, hides internals
Deployment Fat JARs Custom runtime via jlink, jmod

Example Run

# Using classpath
java -cp user.jar:payment.jar com.example.Main

# Using module path
java --module-path mods -m com.example.payment/com.example.Main

On the module path, only what’s explicitly exported is visible.


Pitfalls and Misuse Cases

  1. Classpath/Module Path Confusion – Mixing them incorrectly leads to “class not found” errors.
  2. Overusing Automatic Modules – JARs without descriptors become automatic modules but may break builds.
  3. Open Everything – Declaring open module bypasses encapsulation and defeats JPMS’s purpose.
  4. Migration Pain – Non-modular third-party libraries complicate adoption.

Best Practices

  • Always create clear module-info.java files for your own code.
  • Avoid over-modularizing (too many small modules add complexity).
  • Use requires transitive carefully to re-export stable dependencies.
  • Keep reflection access (opens) limited to frameworks that truly need it.
  • Use jlink to create lean runtimes for Docker/Kubernetes.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced: modules, module-info.java, modular JDK
  • Java 11 → Tooling refinements for javac, jlink
  • Java 17 → Performance and security improvements for JPMS
  • Java 21 → No significant updates across Java versions for this feature

Real-World Example: Modularizing a Legacy App

Imagine a legacy monolith with user, order, and payment packages. Using modules:

// user module
module com.example.user {
    exports com.example.user.api;
}

// order module
module com.example.order {
    requires com.example.user;
    exports com.example.order.api;
}

// payment module
module com.example.payment {
    requires transitive com.example.order;
    exports com.example.payment.api;
}
  • Clear dependency flow: payment → order → user.
  • Encapsulation: Internal classes hidden, only APIs visible.
  • Incremental migration: Teams can modularize piece by piece.

Summary + Key Takeaways

  • A module is a named boundary around code/resources.
  • The module descriptor (module-info.java) defines dependencies, exports, and services.
  • The module path enforces strong encapsulation compared to the classpath.
  • Avoid pitfalls like automatic modules and overuse of open modules.
  • JPMS brings clarity, maintainability, and security for modern Java applications.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath makes everything visible globally, while module path enforces explicit dependencies and access control.

Q2. Why do I get “package is not visible” errors when using modules?
Because only explicitly exported packages in module-info.java are accessible to other modules.

Q3. What is the purpose of requires transitive?
It re-exports dependencies so downstream modules don’t need to declare them separately.

Q4. How do open and opens differ in reflection?

  • open module → All packages open at runtime.
  • opens → Only specific packages open for reflection.

Q5. What are automatic modules, and should I use them?
JARs without descriptors automatically treated as modules. Use only for temporary migration, not long-term.

Q6. How does JPMS improve security compared to the classpath?
By hiding non-exported internals, reducing the attack surface and misuse risk.

Q7. When should I use jlink vs jmod?

  • jlink → Build custom runtime images.
  • jmod → Package modules for distribution.

Q8. Can I migrate a legacy project to modules incrementally?
Yes, start by modularizing a single JAR, then progressively modularize others.

Q9. How do I handle third-party libraries that are not modularized?
Use them on the classpath or as automatic modules until they become modularized.

Q10. Do Java frameworks (Spring, Hibernate) fully support modules?
Not fully. Many rely on reflection, requiring opens for runtime access.