A common mistake developers make when modularizing large Java applications is treating all code as a single giant module. This defeats the purpose of modularization and leads to the same problems the classpath world had—tight coupling, unclear boundaries, and hidden dependencies.
In the Java Platform Module System (JPMS), well-designed applications should be composed of multiple smaller modules with clear responsibilities and boundaries. This ensures encapsulation, easier maintenance, and the ability to deploy leaner custom runtimes using tools like jlink
.
In this tutorial, we’ll explore how to design applications with multiple modules, using practical examples, pitfalls to avoid, and best practices for enterprise-grade modular systems.
Step 1: Identify Logical Boundaries
Start by breaking your application into domains or layers. For example, in an e-commerce application:
com.example.user
→ User management modulecom.example.order
→ Order processing modulecom.example.payment
→ Payment modulecom.example.app
→ Main application module
This separation mirrors real-world domains, reducing coupling.
Step 2: Define Module Descriptors
Each module needs a module-info.java
file.
User Module
module com.example.user {
exports com.example.user.api;
}
Order Module
module com.example.order {
requires com.example.user;
exports com.example.order.api;
}
Payment Module
module com.example.payment {
requires transitive com.example.order;
exports com.example.payment.api;
}
Application Module
module com.example.app {
requires com.example.payment;
}
order
depends onuser
.payment
re-exportsorder
withrequires transitive
.app
depends only onpayment
, but still gains access toorder
anduser
.
Step 3: Compile and Run Multi-Module Applications
Compile:
javac -d out --module-source-path src $(find src -name "*.java")
Run the main application:
java --module-path out -m com.example.app/com.example.app.Main
Step 4: Handle Reflection Needs
Frameworks like Spring or Hibernate may require reflective access. Use opens
instead of open module
.
module com.example.user {
opens com.example.user.entity to hibernate.core;
exports com.example.user.api;
}
This ensures only Hibernate can reflectively access the entities package.
Pitfalls & Misuse Cases
- Single Mega-Module → Eliminates encapsulation benefits.
- Over-Modularization → Splitting too aggressively creates complexity.
- Uncontrolled Transitive Requires → Creates hidden dependencies.
- Exporting Internals → Breaks encapsulation and allows misuse.
- Open Module in Production → Dangerous security exposure.
Best Practices
- Keep modules cohesive around domain responsibilities.
- Export only API packages, never internals.
- Use
requires transitive
only for stable APIs. - Place entities and reflection-heavy classes in separate packages to manage with
opens
. - Regularly review module boundaries as applications evolve.
📌 What's New in Java Versions?
- Java 5 → N/A (modules introduced in Java 9)
- Java 9 → JPMS introduced: multi-module support,
module-info.java
- Java 11 → Better tooling for modular builds (Maven/Gradle integration)
- Java 17 → Security and runtime performance improvements for multi-module apps
- Java 21 → No significant updates across Java versions for this feature
Analogy
Think of a modular application as an office building:
- Each department (module) has its own room and responsibilities.
- Only certain documents (APIs) are shared across rooms.
- Some files are inspected only by auditors (frameworks) with special permission (
opens
). - Without separation, it’s chaos—just like the classpath days.
Summary + Key Takeaways
- Break applications into multiple modules with clear boundaries.
- Use
requires
,exports
, andrequires transitive
wisely to manage dependencies. - Apply
opens
selectively for frameworks requiring reflection. - Avoid pitfalls like mega-modules, over-modularization, and open modules.
- JPMS enables cleaner, safer, and more maintainable applications for enterprise systems.
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 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
→ All packages reflective.opens
→ Only specific packages reflective, often restricted to frameworks.
Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Use temporarily in migration.
Q6. How does JPMS improve security compared to classpath?
By hiding internals, controlling reflection, and enforcing strong boundaries.
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 by modularizing your own code, then dependencies.
Q9. How do I handle third-party libraries that aren’t modularized?
Use them as automatic modules or on the classpath temporarily.
Q10. Do frameworks like Spring or Hibernate fully support modules?
Not fully—many require opens
to enable reflection.