Modularizing Existing Legacy Applications in Java: Step-by-Step Migration Guide

Illustration for Modularizing Existing Legacy Applications in Java: Step-by-Step Migration Guide
By Last updated:

One of the hardest challenges in adopting the Java Platform Module System (JPMS) is dealing with legacy applications built on the classpath. These applications often have thousands of classes, circular dependencies, split packages, and reliance on reflection. When moved to the module path, developers face errors like:

Error: package is not visible
Error: split package detected
Error: module not found

This matters because enterprises still run massive legacy systems. Without modularization, these applications suffer from poor encapsulation, security risks, and difficulties in deployment to cloud-native environments. By modularizing them, organizations can unlock better maintainability, performance, and runtime optimizations with tools like jlink.

In this tutorial, we’ll cover a systematic approach to modularizing legacy applications.


Step 1: Analyze Your Legacy Application

Start by understanding your current project structure.

  • List all JAR dependencies.
  • Identify split packages (same package across multiple JARs).
  • Find reflection-heavy frameworks (Spring, Hibernate, Jackson).

Use jdeps to Analyze Dependencies

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

This shows which classes depend on which packages and helps plan modular boundaries.


Step 2: Move to the Module Path with Automatic Modules

Place existing JARs on the module path. JARs without module-info.java are treated as automatic modules.

java --module-path mods -m com.example.app/com.example.Main
  • Automatic modules get names from JAR filenames.
  • They bridge the gap during migration but should be replaced later.

Step 3: Incrementally Add module-info.java

Start by modularizing your own code.

module com.example.app {
    requires com.example.user;
    requires com.example.order;
    exports com.example.app.api;
}
  • Declare explicit dependencies with requires.
  • Export only necessary APIs with exports.
  • Keep internal packages hidden.

Step 4: Resolve Split Packages

Split packages are not allowed on the module path.

Example Problem

moduleA → com.example.common.Utils
moduleB → com.example.common.Logger

Solutions

  1. Refactor Packages → Move classes into unique packages.
  2. Merge into a Common Module → Combine into com.example.common.
  3. Use Services → Define service APIs and implementations in different modules.

Step 5: Handle Reflection Issues

Frameworks break if reflection is blocked. Use opens carefully.

module com.example.user {
    opens com.example.user.entity to hibernate.core;
    exports com.example.user.api;
}
  • Avoid open module in production (too broad).
  • Use targeted opens for frameworks.

Step 6: Test, Compile, and Run Modularized Code

Compile modular sources:

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

Run application:

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

Once modularized, create lean runtimes:

jlink --module-path $JAVA_HOME/jmods:out --add-modules com.example.app --output custom-runtime
  • jlink builds slim runtimes.
  • jmod packages modules for distribution.

Pitfalls & Misuse Cases

  1. Keeping everything on classpath → No modular benefits.
  2. Relying too long on automatic modules → Fragile, not future-proof.
  3. Exporting all packages → Breaks encapsulation.
  4. Using open module liberally → Security risks.
  5. Ignoring split packages → Blocks modularization.

Best Practices

  • Modularize gradually: start with core code, then dependencies.
  • Keep APIs and implementations in separate modules.
  • Export only API packages, never internals.
  • Use requires transitive sparingly.
  • Replace automatic modules with fully modularized dependencies.
  • Document the migration process for your team.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced: module descriptors, strict encapsulation, service loader updates
  • Java 11 → Better tooling (jdeps, IDE support)
  • Java 17 → Security and runtime refinements for modular systems
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of modularizing legacy apps like renovating an old office building:

  • On the classpath, everyone had access to every room (chaotic, insecure).
  • On the module path, each department has its own office, with access badges for only what’s needed.
  • This reorganization makes the building safer and more efficient.

Summary + Key Takeaways

  • Legacy applications must be modularized gradually.
  • Start with automatic modules, then move to explicit module-info.java.
  • Resolve split packages early, use opens carefully for frameworks.
  • Avoid pitfalls like over-exporting or keeping everything on classpath.
  • Modularization enables security, maintainability, and optimized runtimes.

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 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.

Q4. How do open and opens differ in reflection?

  • open module opens all packages.
  • opens opens specific packages selectively.

Q5. What are automatic modules, and should I use them?
They’re JARs treated as modules. Good for migration, but not a long-term solution.

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

Q7. When should I use jlink vs jmod?

  • jlink → Build custom runtime images.
  • jmod → Package and distribute modules.

Q8. Can I migrate a legacy project incrementally?
Yes. Start small, modularize one JAR at a time.

Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or on the classpath. Replace with modular versions later.

Q10. Do frameworks like Spring or Hibernate fully support modules?
Not fully; many require opens for reflection.