Creating Your First Java Module: A Complete Step-by-Step Guide for Beginners and Experts

Illustration for Creating Your First Java Module: A Complete Step-by-Step Guide for Beginners and Experts
By Last updated:

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 the HelloWorld 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 the out directory.
  • --module-source-path → Tells javac 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 → Runs HelloWorld from the com.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

  1. Forgetting exports – If com.example.hello doesn’t export its package, com.example.greetings cannot access HelloWorld.
  2. Classpath/Module Path mix-ups – Mixing them incorrectly causes errors. Stick to one.
  3. Automatic modules – Avoid depending on unnamed/automatic modules long-term.
  4. 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 and jlink 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.