One of the first challenges developers encounter when migrating to the Java Platform Module System (JPMS) is the difference between the classpath and the module path. A program that runs perfectly with the classpath often fails with cryptic errors like “package is not visible” or “module not found” when using the module path.
This confusion arises because the classpath and module path enforce completely different rules for visibility and dependency management. Understanding these differences is critical when modularizing legacy systems, building microservices, or deploying containerized applications with custom runtime images (jlink).
In this tutorial, we’ll break down the differences between the classpath and module path, show real-world examples, explain pitfalls, and highlight best practices.
The Classpath: Legacy Behavior
The classpath is how Java has historically located classes and resources.
Characteristics
- All JARs and classes are loaded into a single global namespace.
- Every class is visible to every other class.
- No real notion of boundaries between libraries.
- Errors like
ClassNotFoundException
orNoClassDefFoundError
appear at runtime.
Example Run
java -cp libs/*:app.jar com.example.Main
Here, all classes in libs/*
and app.jar
are globally visible.
The Module Path: Modern JPMS Behavior
Introduced in Java 9, the module path enforces stronger encapsulation.
Characteristics
- Classes are organized into modules defined by
module-info.java
. - Only explicitly exported packages are visible.
- Dependencies must be declared with
requires
. - Errors appear at compile-time instead of runtime.
Example Run
java --module-path mods -m com.example.app/com.example.Main
Here, only what’s exported from other modules is accessible.
Direct Comparison: Classpath vs Module Path
Feature | Classpath | Module Path |
---|---|---|
Visibility | All classes globally visible | Only exported packages visible |
Dependencies | Implicit (everything can access all) | Explicit via requires in module-info.java |
Error Detection | Mostly runtime | Compile-time visibility checks |
Encapsulation | Weak – no true hiding of internals | Strong – non-exported packages hidden |
Deployment | Fat JARs | Lean runtime images via jlink and jmod |
Real-World Example
Consider two modules: user
and order
.
Using Classpath
java -cp user.jar:order.jar com.example.order.OrderApp
- Any package in
user
is accessible toorder
, even internal ones. - Accidental coupling is common.
Using Module Path
module-info.java
for user
:
module com.example.user {
exports com.example.user.api;
}
module-info.java
for order
:
module com.example.order {
requires com.example.user;
}
- Only
com.example.user.api
is visible toorder
. - Internal details like
com.example.user.internal
remain hidden.
Pitfalls When Migrating
- Mixing Classpath and Module Path – Leads to “split package” or “module not found” errors.
- Forgetting Exports – Causes “package is not visible” errors.
- Automatic Modules – Fragile when relying on third-party non-modular JARs.
- Classpath Mindset – Developers expect everything to be accessible as before.
Best Practices
- Export only public API packages, not internals.
- Keep dependencies explicit with
requires
. - Use
requires transitive
carefully to avoid hidden couplings. - Avoid mixing classpath and module path in large systems.
- Use build tools like Maven/Gradle for multi-module projects.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced:
module-info.java
, module path vs classpath distinction - Java 11 → Better tooling and IDE integration for module path
- Java 17 → Minor refinements in performance and security for JPMS
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of classpath as an open office where every employee can access every file cabinet—chaotic, but convenient.
The module path is like having separate departments with controlled access:
- HR exports policies.
- Finance requires HR for payroll.
- Internals stay private to each department.
This structure prevents chaos and enforces clear boundaries.
Summary + Key Takeaways
- The classpath makes everything globally visible, causing accidental dependencies.
- The module path enforces encapsulation and explicit dependencies.
- Migrating requires updating code to use
module-info.java
. - Avoid pitfalls like overusing automatic modules or mixing classpath and module path.
- JPMS ensures cleaner, safer, and more maintainable Java projects.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath is global and implicit; module path is explicit with exports/requires.
Q2. Why do I get “package is not visible” errors when using modules?
Because the package wasn’t exported in module-info.java
.
Q3. What is the purpose of requires transitive
?
It re-exports dependencies so downstream modules don’t need to declare them.
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?
They’re non-modular JARs treated as modules. Useful for migration, but brittle.
Q6. How does JPMS improve security compared to classpath?
By hiding non-exported packages and limiting reflection.
Q7. When should I use jlink
vs jmod
?
jlink
builds custom runtime images.jmod
packages modules for distribution.
Q8. Can I migrate a legacy project to modules incrementally?
Yes, start with one JAR/module, keep others on classpath temporarily.
Q9. How do I handle third-party libraries that aren’t modularized?
Use them on classpath or as automatic modules until official support exists.
Q10. Do frameworks like Spring/Hibernate fully support modules?
Not fully; many require opens
for reflection.