Using requires transitive in Java Modules: Dependency Propagation Explained with Real Examples

Illustration for Using requires transitive in Java Modules: Dependency Propagation Explained with Real Examples
By Last updated:

A frequent headache developers face when working with Java Modules is the “package not visible” error, even though the dependency exists somewhere in the project. The culprit? Forgetting to propagate dependencies correctly. On the classpath, everything was accessible globally, but with JPMS, you must explicitly declare dependencies.

This is where requires transitive comes in. It allows dependency propagation so that downstream modules automatically gain access to the dependencies of the modules they depend on. This is particularly important in large enterprise systems, microservices, and layered architectures, where exposing stable APIs to multiple modules is critical.


What is requires transitive?

The requires transitive directive in module-info.java re-exports a dependency. Any module that depends on your module will also have access to the re-exported dependency without declaring it explicitly.

Example

// order module descriptor
module com.example.order {
    requires transitive com.example.user;
    exports com.example.order.api;
}

Here:

  • order depends on user.
  • By marking it as requires transitive, any module that depends on order will also automatically get access to user.

Real-World Example

Imagine a shopping application with three modules:

  1. User Module (com.example.user)
module com.example.user {
    exports com.example.user.api;
}
  1. Order Module (com.example.order)
module com.example.order {
    requires transitive com.example.user;
    exports com.example.order.api;
}
  1. Payment Module (com.example.payment)
module com.example.payment {
    requires com.example.order;
    exports com.example.payment.api;
}

What happens?

  • payment depends on order.
  • Since order uses requires transitive for user, the payment module automatically gets access to user APIs.
  • Without transitive, payment would have to declare requires com.example.user; separately.

This reduces boilerplate and ensures dependency propagation across layers.


Pitfalls and Misuse Cases

  1. Overusing requires transitive → Leads to unnecessary exposure of internals and hidden coupling.
  2. Leaky Abstractions → If a module re-exports too much, downstream modules may accidentally rely on unintended APIs.
  3. Version Conflicts → Complex graphs with transitive dependencies can cause ambiguity when versions differ.
  4. Wrong Placement → Declaring requires transitive in every module defeats its purpose. Reserve it for stable API modules.

Best Practices

  • Use requires transitive for API modules that are widely reused.
  • Avoid applying it to implementation details or unstable modules.
  • Keep module boundaries clear to prevent dependency sprawl.
  • Document why a dependency is made transitive in project guidelines.
  • Regularly review module-info.java to ensure only intentional propagation.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced with requires transitive support
  • Java 11 → Better tooling to detect dependency issues in modular apps
  • Java 17 → Minor refinements for JPMS performance/security checks
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of modules as office departments:

  • The User Department provides employee data.
  • The Order Department depends on User and shares its reports with Finance.
  • The Payment Department depends on Order, and thanks to requires transitive, it automatically gets access to User data without needing a direct subscription.

This ensures smooth workflows without redundant requests.


Summary + Key Takeaways

  • requires transitive propagates dependencies to downstream modules.
  • It’s ideal for shared API modules in layered systems.
  • Avoid overuse, as it can create tight coupling and leaky abstractions.
  • JPMS enforces explicit boundaries, making codebases cleaner, safer, and maintainable.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally; module path enforces explicit requires/exports.

Q2. Why do I get “package is not visible” errors when using modules?
Because the dependency wasn’t explicitly required or re-exported via requires transitive.

Q3. What is the purpose of requires transitive?
It propagates dependencies so downstream modules don’t need to declare them separately.

Q4. How do open and opens differ in reflection?

  • open module opens everything.
  • opens opens specific packages, often restricted to frameworks.

Q5. What are automatic modules, and should I use them?
JARs without descriptors treated as modules. Use temporarily during migration, not in production.

Q6. How does JPMS improve security compared to classpath?
By hiding internals, controlling what’s exported, and limiting reflection.

Q7. When should I use jlink vs jmod?

  • jlink builds slim runtime images.
  • jmod packages modules for distribution.

Q8. Can I migrate a legacy project to modules incrementally?
Yes, modularize one JAR at a time and use automatic modules as a bridge.

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

Q10. Do frameworks like Spring/Hibernate fully support modules?
Not fully. Many require opens for reflection to function correctly.