DEV Community

Vinod Satpute
Vinod Satpute

Posted on

Modules in Java

Cover Image

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;
}
Enter fullscreen mode Exit fullscreen mode
  • module com.example.myapp: Defines a module with the name com.example.myapp.
  • requires java.sql: Declares a dependency on the java.sql module.
  • requires com.example.utils: Depends on another custom module.
  • exports com.example.myapp.api: Makes the com.example.myapp.api package 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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

TextUtils.java:

package com.example.utils.text;

public class TextUtils {
    public static String toUpperCase(String input) {
        return input.toUpperCase();
    }
}
Enter fullscreen mode Exit fullscreen mode

Module: com.example.myapp

module-info.java:

module com.example.myapp {
    requires com.example.utils;
    exports com.example.myapp.api;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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-module option during transition.
  • Gradually add module-info.java files 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 opens Directive 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;
}
Enter fullscreen mode Exit fullscreen mode
  • Create Test Modules: Tests can be organized in separate modules that declare requires on 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)