One of the biggest hurdles developers face when adopting Java’s module system (JPMS) is figuring out how to compile and run modular applications using the familiar javac
and java
commands. Code that works fine on the classpath suddenly throws “package is not visible” or “module not found” errors when moved to the module path.
This confusion matters in real-world applications where enterprises modularize legacy monoliths, microservices rely on strong encapsulation, and deployments require lightweight runtime images (jlink
). Without understanding how javac
and java
work with modules, projects risk runtime failures, broken builds, and wasted developer hours.
In this tutorial, you’ll learn exactly how to compile and run modular programs step by step, with practical examples, pitfalls, and best practices.
Step 1: Setting Up the Project Structure
Consider a simple hello
module project:
hello-world/
├─ src/
│ └─ com.example.hello/
│ ├─ module-info.java
│ └─ com/example/hello/HelloWorld.java
module-info.java
module com.example.hello {
exports com.example.hello;
}
HelloWorld.java
package com.example.hello;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JPMS!");
}
}
Step 2: Compiling with javac
Compile the source into the out
directory:
javac -d out --module-source-path src $(find src -name "*.java")
-d out
→ Output directory for compiled classes.--module-source-path src
→ Tellsjavac
where to find modules.$(find src -name "*.java")
→ Compiles all Java files.
The output folder will look like this:
out/
└─ com.example.hello/
├─ module-info.class
└─ com/example/hello/HelloWorld.class
Step 3: Running with java
Run the compiled module:
java --module-path out -m com.example.hello/com.example.hello.HelloWorld
--module-path out
→ Where modules are located.-m com.example.hello/com.example.hello.HelloWorld
→ RunHelloWorld
incom.example.hello
.
Expected output:
Hello, JPMS!
Step 4: Adding a Dependent Module
Let’s create another module greetings
that depends on com.example.hello
:
greetings/
├─ src/
│ └─ com.example.greetings/
│ ├─ module-info.java
│ └─ com/example/greetings/Greet.java
module-info.java
module com.example.greetings {
requires com.example.hello;
}
Greet.java
package com.example.greetings;
import com.example.hello.HelloWorld;
public class Greet {
public static void main(String[] args) {
HelloWorld.main(args);
}
}
Compile both modules
javac -d out --module-source-path src $(find src -name "*.java")
Run the greetings module
java --module-path out -m com.example.greetings/com.example.greetings.Greet
Common Pitfalls
- Mixing Classpath and Module Path – Leads to “module not found” errors. Stick to one.
- Forgetting Exports – If
com.example.hello
doesn’t export its package,Greet.java
won’t compile. - Using Automatic Modules Excessively – Fragile and should only be used temporarily.
- Incorrect
--module-path
– Missing directories cause runtime errors.
Best Practices
- Always use explicit
exports
to control visibility. - Use
requires transitive
carefully to re-export stable dependencies. - Structure projects with clear module boundaries.
- Avoid over-modularization of small projects.
- Use build tools (Maven/Gradle) for multi-module projects once concepts are clear.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced: module-info.java, module path support in
javac
andjava
- Java 11 → Improved tooling for compiling/running modular applications
- Java 17 → Minor performance/security refinements in JPMS tooling
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of modules as secure office departments:
javac
is like building the office with walls, deciding what each department shares.java
is like letting employees move through those offices, following access rules.
Without clear boundaries, everyone barges into every office—chaos. With modules, only approved doors are open.
Summary + Key Takeaways
- Use
javac
with--module-source-path
to compile modular code. - Run modules with
java --module-path ... -m module/class
. - Be mindful of exports, requires, and module visibility.
- Avoid pitfalls like mixing classpath/module path or overusing automatic modules.
- JPMS enables clean, secure, maintainable builds and deployments.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit dependencies.
Q2. Why do I get “package is not visible” errors when using modules?
Because you didn’t export the package in module-info.java
.
Q3. What is the purpose of requires transitive
?
To re-export dependencies, making them available to downstream modules.
Q4. How do open
and opens
differ in reflection?
open module
opens everything.opens
opens specific packages, often to frameworks.
Q5. What are automatic modules, and should I use them?
JARs without module descriptors treated as modules. Use them temporarily for migration only.
Q6. How does JPMS improve security compared to classpath?
By hiding internals and restricting reflection unless explicitly opened.
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, modularize one JAR at a time, leaving others on the classpath.
Q9. How do I handle libraries that aren’t modularized?
Use them as automatic modules or keep them on the classpath.
Q10. Do frameworks like Spring/Hibernate fully support modules?
Partial support. They often require opens
for reflective access.