Introduction to Java Modules: Why Modular Programming Matters

Illustration for Introduction to Java Modules: Why Modular Programming Matters
By Last updated:

One of the most common frustrations developers face when first adopting Java’s module system is the “package is not visible” error. Suddenly, a class that worked perfectly on the classpath refuses to compile when switched to the module path. This confusion often stems from not understanding how modules control accessibility compared to the traditional classpath model.

In large enterprise systems or modern microservices, this matters because modularization enforces strong boundaries, reduces accidental coupling, and enables secure, maintainable, and deployable systems. With tools like jlink, developers can even ship custom lightweight runtimes containing only the modules they need — critical for containerized deployments and cloud-native applications.


What are Java Modules?

Java Modules were introduced in Java 9 as part of the Java Platform Module System (JPMS). A module is a named, self-describing collection of code and data that declares its dependencies and the packages it exposes to others.

Key Concepts

  • module-info.java → The descriptor file that defines module metadata.
  • requires → Declares dependencies on other modules.
  • exports → Specifies which packages are accessible outside the module.
  • module path → Replacement for the traditional classpath with stronger encapsulation.
  • services → Supports service loading via provides and uses.

Example

// module-info.java
module com.example.payment {
    requires com.example.user;
    exports com.example.payment.api;
    provides com.example.payment.spi.PaymentProcessor with com.example.payment.impl.DefaultProcessor;
}

Here:

  • requires com.example.user → Declares dependency.
  • exports com.example.payment.api → Makes only the api package available externally.
  • provides ... with ... → Defines a service implementation for SPI.

Why Modular Programming Matters

Think of modules as office departments: each department (module) controls what resources it shares, while still collaborating with others through formal policies. Without such boundaries, projects turn into a messy “all-access office” where every employee can interfere with everything.

Benefits

  1. Stronger Encapsulation – Packages not exported remain internal.
  2. Clear Dependencies – Explicit declarations prevent hidden coupling.
  3. Security Improvements – Reduces attack surface by hiding internals.
  4. Smaller Deployments – Create custom runtimes with only required modules.
  5. Incremental Migration – Legacy applications can be modularized gradually.

Classpath vs Module Path

Feature Classpath Module Path
Dependency Handling All classes visible everywhere Explicit requires controls access
Encapsulation Weak, accidental access possible Strong, non-exported packages hidden
Error Detection Runtime (ClassNotFoundException) Compile-time (visibility errors)
Deployment Fat JARs Custom runtimes via jlink, jmod

Pitfalls & Misuse Cases

  1. Over-Modularization – Splitting into too many modules increases complexity.
  2. Automatic Modules – Useful for migration, but fragile (names derived from JARs).
  3. Reflection Issues – Non-exported packages are inaccessible unless opened explicitly.
  4. Third-Party Libraries – Not all popular libraries are fully modularized yet.

Best Practices

  • Use explicit exports instead of opening everything.
  • Prefer requires transitive carefully (only for stable APIs).
  • Use open module sparingly — only when frameworks (like Hibernate) need reflection.
  • Combine JPMS with build tools (Maven/Gradle) for reliable dependency management.
  • Use jlink to reduce runtime size in Docker/Kubernetes deployments.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → Introduction of JPMS, module-info.java, modular JDK
  • Java 11 → Tooling improvements (javac, jlink refinements)
  • Java 17 → Minor performance and security refinements
  • Java 21 → No significant updates across Java versions for this feature

Real-World Example: Migrating a Monolith

Suppose you have a legacy monolithic application with user, order, and payment packages. By modularizing:

module com.example.user {
    exports com.example.user.api;
}

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

module com.example.payment {
    requires transitive com.example.order;
    exports com.example.payment.api;
}
  • Clear dependency chain (payment depends on order, which depends on user).
  • Internal details of each module remain hidden.
  • Only APIs are exposed, simplifying maintenance and upgrades.

Summary + Key Takeaways

  • Java Modules solve long-standing classpath and dependency issues.
  • module-info.java is central to defining module structure.
  • Use exports/requires for strong encapsulation.
  • Avoid overusing automatic modules and open modules.
  • JPMS improves maintainability, security, and deployability for modern apps.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath exposes everything globally, while module path enforces explicit dependencies and encapsulation.

Q2. Why do I get “package is not visible” errors when using modules?
Because only exported packages are accessible; internal packages remain hidden.

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

Q4. How do open and opens differ in reflection?

  • open module → All packages accessible at runtime.
  • opens → Specific package is accessible for reflection.

Q5. What are automatic modules, and should I use them?
They’re unnamed JARs treated as modules; useful in migration but brittle long-term.

Q6. How does JPMS improve security compared to the classpath?
By hiding internal APIs, reducing attack vectors and accidental misuse.

Q7. When should I use jlink vs jmod?

  • jlink → Build custom runtime images.
  • jmod → Package modules for distribution.

Q8. Can I migrate a legacy project to modules incrementally?
Yes, start with a single modular JAR and gradually modularize others.

Q9. How do I handle third-party libraries that are not modularized?
Place them on the classpath or use automatic modules until official support exists.

Q10. Do Java frameworks (Spring, Hibernate) fully support modules?
Support is partial; many use reflection requiring opens for runtime compatibility.