Understanding Modules in Java
Java modules represent a significant shift in the way Java applications are structured, built, and maintained. Introduced in Java 9 as part of the Java Platform Module System (JPMS), modules address long-standing issues related to scalability, maintainability, and security in large codebases. For developers, testers, and automation enthusiasts, mastering Java modules is essential for producing robust, modular applications that are easier to manage and evolve.
This blog post dives into what Java modules are, why they matter, and how to effectively use them in your projects.
Why Modules Matter in Java
Before the introduction of modules, Java applications relied heavily on packages and the classpath for organization. While packages grouped related classes, the classpath offered no strict encapsulation or clear boundaries between different parts of an application or between different applications altogether.
Some key pain points with pre-module Java environments:
- Classpath Hell: Managing dependencies manually and in large projects often led to conflicts, versioning issues, or accidental usage of internal APIs.
- Lack of Strong Encapsulation: Packages do not enforce strict boundaries. Any public class is accessible anywhere on the classpath.
- Difficult Maintenance and Scalability: Large monolithic JAR files could grow unwieldy, complicating updates, refactoring, and deployment.
- Runtime Performance and Security Issues: Eager loading of all classes on the classpath could waste resources or expose internal APIs unintentionally.
Modules were introduced as a solution to these problems. They enable developers to define clear dependencies and accessibility rules at a higher level than packages, improving code modularity, security, and maintainability.
What is a Module in Java?
A Java module is a self-contained unit of code that explicitly declares which other modules it depends on and which packages it exports to the outside world. This declaration occurs in a special file called module-info.java inside the module.
Key Characteristics of a Java Module
- Strong Encapsulation: Modules directly control the visibility of packages to other modules.
- Explicit Dependencies: Modules declare which other modules they require to function.
- Improved Security: Internal APIs can be hidden from outside access.
- Better Performance: The JVM can optimize module loading based on explicit dependencies.
Anatomy of a Java Module
Every Java module includes a descriptor file named module-info.java. This file lives at the root of the module hierarchy and defines the module’s name, its dependencies, and what it exposes.
Example: module-info.java
module com.example.myapp {
requires java.sql;
requires com.example.utils;
exports com.example.myapp.api;
}
-
module com.example.myapp: Defines a module with the namecom.example.myapp. -
requires java.sql: Declares a dependency on thejava.sqlmodule. -
requires com.example.utils: Depends on another custom module. -
exports com.example.myapp.api: Makes thecom.example.myapp.apipackage accessible to other modules.
Using Modules: Step-by-Step Guide
1. Creating a Module
Start by creating your project directory structure reflecting your module name:
myapp/
└─ src/
└─ com.example.myapp/
├─ module-info.java
└─ com/
└─ example/
└─ myapp/
└─ Main.java
The module-info.java defines the module metadata, while your Java code resides inside the package directories.
2. Writing the Module Descriptor
Define dependencies and exports in module-info.java:
module com.example.myapp {
requires java.base; // Implicitly required, but can be stated explicitly
exports com.example.myapp.api;
}
3. Compiling Modules
Compile your module using the javac compiler with the module path option:
javac -d out --module-source-path src $(find src -name "*.java")
This compiles all modules found under the src directory and places their output in the out directory.
4. Running Modular Applications
Run your modular application specifying the module and main class:
java --module-path out -m com.example.myapp/com.example.myapp.Main
Practical Examples of Modules
Let’s consider a simple project with two modules: com.example.utils and com.example.myapp.
Module: com.example.utils
module-info.java:
module com.example.utils {
exports com.example.utils.text;
}
TextUtils.java:
package com.example.utils.text;
public class TextUtils {
public static String toUpperCase(String input) {
return input.toUpperCase();
}
}
Module: com.example.myapp
module-info.java:
module com.example.myapp {
requires com.example.utils;
exports com.example.myapp.api;
}
Main.java:
package com.example.myapp.api;
import com.example.utils.text.TextUtils;
public class Main {
public static void main(String[] args) {
String result = TextUtils.toUpperCase("hello modules");
System.out.println(result);
}
}
Here, com.example.myapp depends on com.example.utils but only exposes its own API package.
Advantages of Using Java Modules
Better Maintainability
Modules enforce strong encapsulation by default, making code easier to maintain over time. Internal implementation details are hidden, so changes inside a module often do not affect other modules.
Improved Security
Since modules do not expose internal packages unless explicitly exported, sensitive internal APIs remain inaccessible, reducing the attack surface.
Clarity of Dependencies
module-info.java clearly states what dependencies each module requires, simplifying build and deployment processes.
Simplified Large-Scale Projects
Modules break monolithic JARs into smaller, reusable components. Teams can work independently on different modules, fostering better collaboration.
Common Challenges and Best Practices
Dealing with Split Packages
A split package occurs when two modules export the same package name. This is not allowed in the module system and leads to runtime errors.
Best practice: Avoid split packages by reorganizing code so that each package is owned by exactly one module.
Migrating Legacy Projects
Legacy Java projects often have monolithic codebases without module descriptors. Migration requires careful planning:
- Start by modularizing at a high level.
- Use the
--patch-moduleoption during transition. - Gradually add
module-info.javafiles to key modules.
Module Naming Conventions
Module names should follow reverse domain name conventions (e.g., com.company.project). This avoids conflicts and ensures uniqueness.
Modules and Testing
Testing modular applications requires ensuring test code can access the modules under test.
Testing Approaches
-
Use
opensDirective in Module: To allow reflection-based testing frameworks such as JUnit to access non-exported packages.
module com.example.myapp {
exports com.example.myapp.api;
opens com.example.myapp.internal to org.junit.platform.commons;
}
-
Create Test Modules: Tests can be organized in separate modules that declare
requireson the modules they test.
Conclusion
Java modules introduce a modern, scalable approach to organizing applications and libraries. They resolve many longstanding challenges around dependency management, encapsulation, and security. By explicitly declaring dependencies and controlling package exports, modules enhance code clarity, maintainability, and runtime performance.
For developers and testers, embracing the module system offers a structured foundation for building reliable and modular Java applications. Starting with proper module descriptor files and understanding the JVM’s module requirements will ensure your projects are future-proof and easier to manage.
As you adopt modules, focus on clear dependencies, avoid split packages, and leverage the module system to safeguard your internal APIs. This will result in cleaner codebases, smoother collaboration, and improved application robustness.

Top comments (0)