Table of contents
- Book Content
- Chapter 1 - Introduction to Modules
- Chapter 2 - The First Module
- Chapter 3 - Module Dependencies
- Chapter 4 - JDK with Modules
- Chapter 5 - Using APIs from the Java Platform
- Chapter 6 - Module Resolution
- Chapter 10 - Migrating to Java 9 Modules
- Chapter 11 - Migration Strategy
- Practical Examples
- References
- Changelog
Modular Programming in Java 9 - Build large scale applications using Java modularity and Project Jigsaw
The content here is under the Attribution 4.0 International (CC BY 4.0) license
Discover the comprehensive guide to constructing large-scale applications using Java modularity and Project Jigsaw. Learn the steps to migrate to Java 9 modules by segmenting code, enhancing encapsulation, and creating well-defined interfaces. This guide helps you understand the benefits of modular programming, address the pain points of maintaining large codebases, and explore the new features introduced in Java 9, including the Java Platform Module System (JPMS).
Book Content
Chapter 1 - Introduction to Modules
Objectives
- Pain points in maintaining large codebases
- Modularity approaches prior to Java 9
- The Project Jigsaw
- JPMS - Java Platform Module System
Content
Modular programming is a widely recognized software design approach that achieves encapsulation and well-defined interfaces. Modules provide the ability to hide internal implementation details. Java never had native language support for modules until Java 9, which introduced the new concept of modules at the language level.
Prior to Java 9, applications could only use encapsulation at the package level and class/method level. This limitation meant that one developer could create an internal library, but consumers could reference the internal implementation without any compiler-enforced restriction. Additionally, Java applications had no way to know if a dependency was loaded until runtime when it was required to be used. Traditional applications were bundled in a single JAR file.
Java modules were accepted in JSR 376
Chapter 2 - The First Module
Creating the first module from scratch introduces fundamental concepts. When using IDEs like IntelliJ, tools often generate extra files and configuration that the book examples do not require. An important behavioral change occurs once a module is introduced. The application no longer permits classes to be created without an explicit package declaration.
Chapter 3 - Module Dependencies
Objectives
- Split code into two or more modules
- Understand module interdependencies
- Understand differences between classpath and module path initialization in Java 9+
Content
Creating a module in IDEs is accomplished through file dialogs and menu options. An important distinction is that marking a class as public within a module does not automatically grant access to other modules. Modules work with explicit dependencies. Each module must declare which dependencies can be accessed externally via exports, and each module must declare which other modules it depends on via requires.
The concept of the classpath has existed for decades, but modules introduce a new approach. Given the module path, the compiler and runtime can determine where all classes are located. When executing code, dependency verification no longer occurs only at runtime; verification happens during virtual machine initialization, catching configuration errors earlier.
Module versioning is not possible in JPMS, which differs from frameworks like OSGi that support versioned modules.
Chapter 4 - JDK with Modules
The Java Development Kit restructure under Project Jigsaw organized internal classes into distinct modules. Previously, the monolithic rt.jar file contained nearly all classes needed for runtime execution. Shipping large JAR files created deployment challenges because applications often used only a subset of available classes. Java 8 introduced compact profiles as an optimization; modules provide a more fundamental solution by decomposing the JDK into purpose-specific modules (java.logging, java.desktop, java.transaction).
Internal versus public APIs present a historical challenge. The sun.* packages were never intended for public use, but Javaβs backward compatibility commitment created ongoing pressure. Project Jigsaw addressed this by grouping internal classes into separate modules, prevented default import of sun.* packages, and enforced encapsulation at the language level.
The Java platform automatically imports classes from java.lang. Use java --list-modules to view available modules in your Java installation.
Chapter 5 - Using APIs from the Java Platform
Objectives
- Create additional modules for practical use cases
- Add logging via
java.logging - Read contacts from XML
- Understand that dependencies should form a directed acyclic graph without circular dependencies
- Add UI support with JavaFX
Content
Without explicitly requiring the necessary modules, applications fail to function correctly. This requirement enforcement is an improvement over pre-module Java where missing dependencies might not be detected until the class was actually needed.
Chapter 6 - Module Resolution
Objectives
- Examine nuances of module readability, resolution, and accessibility
Content
Readability describes how each module interacts with other modules. Circular dependencies are not allowed in JPMS, which enforces acyclic module dependency graphs. These graphs describe relationships between modules and determine loading order. The rules governing accessibility ensure strong encapsulation and prevent unintended module coupling.
Chapter 10 - Migrating to Java 9 Modules
Objectives
- Establish a migration path for legacy codebases
Content
The first step is to compile existing code with Java 9 or greater without introducing modules. Surprisingly, code written for earlier Java versions continues to function in Java 9+ even without modules. Java supports this behavior to accommodate legacy codebases; this is known as the unnamed module.
The unnamed module has access to all classes and packages by default. The Java Dependency Analysis Tool (jdeps) statically examines code to identify usage of internal API classes. Java provides compiler flags to support gradual migration. If code lacks access to a required module, parameters can be passed to add this access temporarily: --add-exports and --add-opens. The unnamed module is referenced by ALL-UNNAMED. The flag --permit-illegal-access disables all modularity features as a last resort.
Chapter 11 - Migration Strategy
Objectives
- Establish migration strategy for legacy code
- Understand multi-release JAR files
- Gradually modularize an application bit by bit
Content
Real-world applications are complex and require careful planning. Gradual migration is preferred over attempting complete modularization in a single effort. The modularization journey typically involves three layers: the application layer, frameworks, and the Java platform itself. A systematic approach addresses each layer, starting with dependency analysis and proceeding incrementally.
Practical Examples
The following examples are supplementary material and are not from the book. They complement the concepts covered in βModular Programming in Java 9β by providing concrete, hands-on demonstrations of JPMS in practice. These examples illustrate common modularity patterns and migration scenarios to deepen understanding beyond the bookβs content.
Basic Module Structure
A typical modular project structure looks like this:
myapp/
βββ src/
β βββ com.example.core/
β β βββ module-info.java
β β βββ com/example/core/Calculator.java
β βββ com.example.ui/
β βββ module-info.java
β βββ com/example/ui/Main.java
βββ build/
Example 1: Core Utility Module
The core module provides basic functionality and explicitly exports its API:
module-info.java:
module com.example.core {
exports com.example.core.api;
}
com/example/core/api/Calculator.java:
package com.example.core.api;
public class Calculator {
public static int add(int a, int b) {
return a + b;
}
}
Note that com.example.core.internal packages are not exported and remain hidden from other modules.
Example 2: Module with Dependencies
A UI module that depends on the core module:
module-info.java:
module com.example.ui {
requires com.example.core;
exports com.example.ui.app;
}
com/example/ui/app/Main.java:
package com.example.ui.app;
import com.example.core.api.Calculator;
public class Main {
public static void main(String[] args) {
int result = Calculator.add(5, 3);
System.out.println("Result: " + result);
}
}
Example 3: Analyzing Dependencies with jdeps
Before migrating a legacy application, use jdeps to identify internal API usage:
# Analyze a JAR file for internal API usage
jdeps --jdk-internals application.jar
# Generate module dependencies
jdeps --generate-module-info . application.jar
This command reveals which internal packages (sun., com.sun.) the application uses, informing migration strategy.
Example 4: Gradual Migration with Unnamed Module
Compile existing code without modules first:
# Compile without requiring module-info.java
javac -d out src/**/*.java
# Run as unnamed module (has access to all modules)
java -cp out com.example.Main
Then gradually introduce modules by adding module-info.java files and using compiler flags during transition:
# Allow temporary access during migration
javac --add-exports java.logging/sun.util.logging.resources=com.example.app \
-d out src/**/*.java
Example 5: Maven Configuration for Modular Projects
Maven can be configured to support modules:
pom.xml:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>9</maven.compiler.source>
<maven.compiler.target>9</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Common Pitfalls
Circular Dependencies: Avoid creating module A that requires B while B requires A. The module system prevents this at initialization.
Missing Exports: Forgetting to export required packages makes them inaccessible to dependent modules, causing compilation errors.
Classpath vs Module Path Confusion: Modules must be on the module path (--module-path), not the classpath. Mixing both can lead to unexpected behavior.
For comprehensive examples and real-world projects, refer to Java modules on GitHub.
References
- Modules with Gradle
- Oracle Java 9 Module System Documentation
- JSR 376: Java Platform Module System
- jdeps - Java Dependency Analysis Tool
Changelog
- Feb 15, 2026: Updated references and added practical examples section