Common Pitfalls and Anti-Patterns in Java Module Usage

Illustration for Common Pitfalls and Anti-Patterns in Java Module Usage
By Last updated:

Many developers adopting the Java Platform Module System (JPMS) assume that simply adding a module-info.java file instantly makes their project modular. The reality is far more complex. Common mistakes—such as mixing classpath and module path, exporting everything, or abusing opens—often lead to broken encapsulation, runtime errors, or even security vulnerabilities.

In large enterprise systems or microservice architectures, these pitfalls can accumulate into costly technical debt. Understanding the most frequent anti-patterns in JPMS usage helps ensure that your modular applications remain secure, maintainable, and future-proof.

Think of JPMS as designing departments in a company. If every department shares all its documents without restrictions, chaos ensues. Proper boundaries must be enforced, and pitfalls avoided.


Pitfall 1: Mixing Classpath and Module Path

Problem

Running part of the app on the classpath and part on the module path leads to inconsistent visibility.
Example:

java --module-path mods -cp lib/* -m com.example.app/com.example.Main

Why It’s Bad

  • Undermines modular boundaries
  • Causes unpredictable runtime behavior
  • Hard to debug dependency issues

Solution

Migrate incrementally but consistently—prefer the module path for all modularized code.


Pitfall 2: Exporting Everything

Problem

Developers often export all packages for convenience:

module com.example {
    exports com.example.core;
    exports com.example.internal; // should not be public
}

Why It’s Bad

  • Breaks encapsulation
  • Exposes internal APIs unnecessarily
  • Makes refactoring dangerous

Solution

Export only public APIs. Keep implementation details hidden.


Pitfall 3: Overusing open or opens

Problem

Declaring entire modules as open:

open module com.example {
    requires java.sql;
}

Why It’s Bad

  • Defeats JPMS encapsulation
  • Gives reflective frameworks unrestricted access
  • Creates long-term security risks

Solution

Use opens selectively for specific packages that require reflection.


Pitfall 4: Overreliance on Automatic Modules

Problem

Placing non-modular JARs on the module path creates automatic modules.
Example:

legacy-lib.jar → automatic module: legacy.lib

Why It’s Bad

  • Module names are unstable (derived from filenames)
  • Exports all packages by default → weak encapsulation
  • Breaks when library updates change JAR naming

Solution

Prefer libraries with module-info.java or add Automatic-Module-Name in the manifest.


Pitfall 5: Misusing requires transitive

Problem

Adding requires transitive everywhere:

module com.example.service {
    requires transitive com.example.core;
}

Why It’s Bad

  • Pollutes dependency graphs
  • Creates unintended coupling between modules
  • Makes maintenance harder

Solution

Use transitive sparingly, only when an API genuinely depends on another API.


Pitfall 6: Ignoring Reflection in Frameworks

Problem

Hibernate, Spring, and similar frameworks fail with:

IllegalAccessException: module does not open package ...

Why It’s Bad

  • Blocks frameworks relying on reflection
  • Leads to runtime crashes

Solution

Use opens in module-info.java or runtime flags:

--add-opens com.example.entities/com.example.entities=org.hibernate.orm.core

Pitfall 7: Overcomplicating Module Structures

Problem

Creating too many small modules:

com.example.util.string
com.example.util.math
com.example.util.io

Why It’s Bad

  • Leads to dependency sprawl
  • Adds overhead to builds and runtime
  • Harder to manage for large teams

Solution

Balance granularity—modules should represent meaningful boundaries, not individual utilities.


What's New in Java Versions?

  • Java 5–8 → N/A (no JPMS)
  • Java 9 → JPMS introduced, modules became part of the JDK
  • Java 11 → Tooling improvements (jdeps, better support for migration)
  • Java 17 → Security and performance refinements for JPMS
  • Java 21 → No significant updates across Java versions for this feature

Real-World Analogy

Using JPMS incorrectly is like leaving all office doors unlocked and giving everyone master keys. While convenient, it leads to confusion, misuse, and potential breaches. Correct usage enforces the right level of access control for maintainability and security.


Summary & Key Takeaways

  • Avoid mixing classpath and module path
  • Export only public APIs, not internals
  • Use opens selectively, not globally
  • Don’t rely heavily on automatic modules
  • Use requires transitive sparingly
  • Balance module granularity
  • Handle reflection-based frameworks explicitly
  • Proper modularization leads to secure, maintainable systems

FAQ: Java Module Pitfalls

1. What is the difference between classpath and module path?
Classpath loads everything without boundaries; module path enforces explicit dependencies.

2. Why do I get “package is not visible” errors?
Because you didn’t export or open the package correctly.

3. What is the purpose of requires transitive?
It passes dependencies along automatically, but should be used sparingly.

4. How do open and opens differ?
open applies to the entire module, while opens applies to specific packages.

5. What are automatic modules, and should I use them?
Temporary tools for non-modular JARs. Avoid in production.

6. How does JPMS improve security over classpath?
By restricting access to non-exported packages and APIs.

7. When should I use jlink vs jmod?
Use jlink for runtime images, jmod for packaging modules.

8. Can I migrate a legacy project incrementally?
Yes, start with core APIs and gradually modularize.

9. How do I handle third-party libraries that aren’t modularized?
Use Automatic-Module-Name or modularize with tools like moditect.

10. Do frameworks like Spring and Hibernate fully support JPMS?
Yes, but they often require opens for reflection-heavy features.