Compiling and Running Modular Programs with javac and java: A Complete Guide

Illustration for Compiling and Running Modular Programs with javac and java: A Complete Guide
By Last updated:

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 → Tells javac 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 → Run HelloWorld in com.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

  1. Mixing Classpath and Module Path – Leads to “module not found” errors. Stick to one.
  2. Forgetting Exports – If com.example.hello doesn’t export its package, Greet.java won’t compile.
  3. Using Automatic Modules Excessively – Fragile and should only be used temporarily.
  4. 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 and java
  • 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.