Migration Guide: Moving from Classpath to Module Path in Java with Real-World Strategies

Illustration for Migration Guide: Moving from Classpath to Module Path in Java with Real-World Strategies
By Last updated:

A common frustration for developers migrating to Java Modules is that applications running perfectly on the classpath suddenly break on the module path. Errors like “package is not visible”, “module not found”, or conflicts with split packages often appear.

This matters because in large enterprise systems, microservices, and cloud-native deployments, strong encapsulation and custom runtime images are essential. Migrating from the classpath to the module path ensures better maintainability, security, and performance—but it requires careful planning.

This migration guide provides a step-by-step process, with code examples, best practices, and pitfalls to avoid.


Step 1: Analyze Your Existing Application

Before modularizing, identify:

  • All existing JAR dependencies.
  • Any split packages (same package across multiple JARs).
  • Libraries that rely heavily on reflection (Spring, Hibernate, Jackson).

Tooling Tip

Use jdeps to analyze dependencies:

jdeps --class-path libs/* app.jar

This shows which packages depend on which modules.


Step 2: Start with Automatic Modules

Place third-party libraries on the module path, even if they are not modularized. JARs without module-info.java become automatic modules.

java --module-path mods -m com.example.app/com.example.Main
  • Automatic modules are a temporary bridge.
  • Module names are derived from JAR filenames (fragile in the long term).

Step 3: Introduce module-info.java for Your Code

Start modularizing your own code by adding a module-info.java file.

Example

module com.example.app {
    requires com.example.user;
    requires com.example.order;
    exports com.example.app.api;
}
  • Declare dependencies with requires.
  • Export only public APIs with exports.
  • Hide internals for better encapsulation.

Step 4: Handle Split Packages

If two JARs declare the same package, JPMS will fail.

Resolution Strategies

  1. Refactor package structure (preferred).
  2. Merge code into a single common module.
  3. Use services (provides and uses) to separate responsibilities.

Step 5: Address Reflection Issues

Frameworks like Spring and Hibernate often fail due to blocked reflection. Use opens to allow controlled access.

module com.example.user {
    opens com.example.user.entity to hibernate.core;
    exports com.example.user.api;
}
  • opens grants reflection to specific frameworks.
  • Avoid using open module unless absolutely necessary.

Step 6: Compile and Run with javac and java

Compile with module awareness:

javac -d out --module-source-path src $(find src -name "*.java")

Run the modular app:

java --module-path out -m com.example.app/com.example.app.Main

Once migration is complete:

  • Use jlink to build custom runtime images.
  • Use jmod to package modules for distribution.
jlink --module-path $JAVA_HOME/jmods:out --add-modules com.example.app --output custom-runtime

Pitfalls & Misuse Cases

  1. Leaving everything on classpath → Defeats modularization.
  2. Overusing automatic modules → Leads to fragile builds.
  3. Exporting all packages → Removes encapsulation benefits.
  4. Open modules in production → Creates security risks.
  5. Ignoring split packages → Compilation/runtime failures.

Best Practices

  • Modularize gradually, starting with your own code.
  • Export only API packages; keep internals hidden.
  • Use requires transitive sparingly for stable APIs.
  • Replace automatic modules with fully modularized dependencies over time.
  • Document your migration steps for team-wide consistency.

📌 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, modular JDK
  • Java 11 → Improved jdeps, jlink, and IDE support
  • Java 17 → Refinements in JPMS security and performance
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of moving from the classpath to the module path like reorganizing a chaotic office:

  • On the classpath, every employee can access every file in every cabinet.
  • On the module path, each department controls its own files and shares only what’s necessary.

This organization reduces confusion and strengthens security.


Summary + Key Takeaways

  • Migration to the module path requires handling automatic modules, split packages, and reflection issues.
  • Start with module-info.java for your own code, then gradually modularize dependencies.
  • Use opens for frameworks, but avoid open module in production.
  • JPMS enables encapsulation, security, and deployment optimization with tools like jlink.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces strict dependencies and exports.

Q2. Why do I get “package is not visible” errors when migrating?
Because the package wasn’t exported in module-info.java.

Q3. What is the purpose of requires transitive?
It re-exports dependencies so downstream modules don’t need to declare them explicitly.

Q4. How do open and opens differ in reflection?

  • open module → All packages are reflective.
  • opens → Specific packages reflective, often restricted to frameworks.

Q5. What are automatic modules, and should I use them?
They’re JARs without descriptors treated as modules. Useful temporarily during migration.

Q6. How does JPMS improve security compared to classpath?
By hiding internals, restricting reflection, and preventing split package conflicts.

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. Start with your code, then modularize dependencies gradually.

Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or place them on the classpath temporarily.

Q10. Do frameworks like Spring or Hibernate fully support modules?
Partial support—most require opens for reflection.