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→ Tellsjavacwhere 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→ RunHelloWorldincom.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.hellodoesn’t export its package,Greet.javawon’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
exportsto control visibility. - Use
requires transitivecarefully 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
javacandjava - 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:
javacis like building the office with walls, deciding what each department shares.javais 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
javacwith--module-source-pathto 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 moduleopens everything.opensopens 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?
jlinkbuilds custom runtime images.jmodpackages 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.