A common frustration developers face when starting with Java modules is this: “My code works perfectly on the classpath, but breaks when I switch to the module path.” Suddenly, IDEs and builds throw visibility errors, especially when classes are not explicitly exported.
This pain is real in enterprise systems and microservices where maintainability, clear boundaries, and security matter. By learning how to create your first Java module, you not only fix these errors but also prepare your applications for future-proof deployment with modular runtimes (jlink
) and better encapsulation.
This guide walks step by step through building your first module, explaining key commands, pitfalls, and best practices.
Step 1: Project Structure
Create a simple project structure for a hello.module
example:
hello-module/
├─ src/
│ └─ com.example.hello/
│ ├─ module-info.java
│ └─ com/example/hello/HelloWorld.java
Step 2: Writing the Module Descriptor
Inside src/com.example.hello/module-info.java
:
module com.example.hello {
exports com.example.hello;
}
module com.example.hello
→ Defines the module name.exports com.example.hello
→ Makes theHelloWorld
class visible to other modules.
Step 3: Creating the Main Class
Inside src/com.example.hello/com/example/hello/HelloWorld.java
:
package com.example.hello;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Java Modules!");
}
}
Step 4: Compiling with Module Path
Compile the module with javac
:
javac -d out --module-source-path src $(find src -name "*.java")
This command:
-d out
→ Places class files in theout
directory.--module-source-path
→ Tellsjavac
where to find module sources.
Step 5: Running the Module
Run the compiled module:
java --module-path out -m com.example.hello/com.example.hello.HelloWorld
Explanation:
--module-path out
→ Location of compiled modules.-m com.example.hello/com.example.hello.HelloWorld
→ RunsHelloWorld
from thecom.example.hello
module.
Step 6: Adding a Dependent Module
Create another module greetings
that requires com.example.hello
:
greetings-module/
├─ 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 together:
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
Pitfalls and Misuse Cases
- Forgetting exports – If
com.example.hello
doesn’t export its package,com.example.greetings
cannot accessHelloWorld
. - Classpath/Module Path mix-ups – Mixing them incorrectly causes errors. Stick to one.
- Automatic modules – Avoid depending on unnamed/automatic modules long-term.
- Overcomplication – Don’t split small projects into too many modules.
Best Practices
- Keep module names consistent with package naming (
com.example.app
). - Always export only what’s necessary (avoid exporting internals).
- Use
requires transitive
carefully when designing libraries. - Limit
opens
to only frameworks (e.g., Hibernate) that require reflection. - Use
jlink
for lean deployments in Docker or microservices.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced:
module-info.java
, module path, modular JDK - Java 11 → Better
javac
andjlink
support - Java 17 → Security refinements and small performance improvements
- Java 21 → No significant updates across Java versions for this feature
Real-World Analogy
Think of modules as departments in an organization:
- The HR department (module) shares only certain policies (exports) with Finance.
- Finance requires HR to process salaries (
requires
). - Both remain independent yet collaborate through clear contracts.
This encapsulation prevents chaos, just like JPMS prevents unwanted access to internal code.
Summary + Key Takeaways
- A module is a named unit with clear dependencies and exports.
module-info.java
defines boundaries and visibility.- Use the module path instead of classpath for modular projects.
- Compile with
--module-source-path
and run with--module-path
. - Avoid pitfalls like unexported packages, over-modularization, and automatic modules.
FAQs
Q1. What is the difference between the classpath and module path?
Classpath makes all classes visible globally, module path enforces explicit exports and dependencies.
Q2. Why do I get “package is not visible” errors when using modules?
Because you forgot to export the package in module-info.java
.
Q3. What is the purpose of requires transitive
?
To re-export dependencies so downstream modules inherit them automatically.
Q4. How do open
and opens
differ in reflection?
open module
→ Entire module is open.opens
→ Only specific packages are open for reflection.
Q5. What are automatic modules, and should I use them?
They’re JARs without descriptors treated as modules. Use temporarily during migration only.
Q6. How does JPMS improve security compared to classpath?
By hiding internals unless explicitly exported, reducing misuse and attack surfaces.
Q7. When should I use jlink
vs jmod
?
jlink
→ Build slim custom runtimes.jmod
→ Package and distribute modules.
Q8. Can I migrate a legacy project to modules incrementally?
Yes. Start by modularizing one JAR, then expand module by module.
Q9. How do I handle third-party libraries that are not modularized?
Use them as automatic modules or place them on the classpath until modularized.
Q10. Do frameworks like Spring or Hibernate fully support modules?
Partial support. They often require opens
to access classes via reflection.