Introduction to Java Modules (Project Jigsaw)

The evolution of Java has always aimed at making the language and platform more efficient, scalable, and easier to manage. One of the most significant changes introduced in Java 9 was the module system, famously known as Project Jigsaw. This feature has transformed how I organize, build, and maintain Java applications, especially large ones, by providing a clear structure for modular development.

In this article, I will introduce the concept of Java modules, explain why they were introduced, and walk through the essentials of using the Java module system. I’ll share practical insights from my experience working with modules and highlight some challenges and best practices for effective modular programming.

Why Modularize Java Applications?

Before Java 9, the Java platform itself and most applications relied on the classpath, a simple but somewhat limited mechanism for loading classes and resources. The classpath worked well for many years but had some drawbacks:

  • Monolithic JAR files: Large applications often packaged everything into huge JARs, making it difficult to manage dependencies and reuse parts.
  • Dependency conflicts: Classpath made it easy for different libraries to bring conflicting versions of the same class, causing the infamous “classpath hell.”
  • Poor encapsulation: Any public class was accessible to all code on the classpath, which meant you couldn’t hide internal implementation details effectively.
  • Platform bloat: The Java runtime included a vast number of APIs, many of which were rarely used in typical applications, leading to unnecessarily large runtime footprints.

Project Jigsaw’s module system aimed to address these problems by introducing a new way to group code, declare dependencies, and control visibility, making applications more maintainable and scalable.

What Is a Java Module?

A module is a self-contained unit of code and resources with a well-defined boundary. It explicitly declares what it requires from other modules and what it exports to them. Think of a module as a strong encapsulation mechanism beyond traditional Java packages.

Modules have their own metadata, which specifies:

  • Dependencies: Other modules it needs.
  • Exports: Packages it makes accessible to other modules.
  • Services: Optional service providers it offers or consumes.

This explicit structure helps the Java runtime enforce strong encapsulation and reliable configuration.

Anatomy of a Module: The module-info.java File

At the heart of every module lies a descriptor file named module-info.java. This file lives at the root of the module’s source folder and defines the module’s name, its dependencies, and exported packages.

Here’s a simple example:

java module com.example.myapp {
    requires java.sql;
    requires com.example.utils;
    exports com.example.myapp.api;
}

In this declaration:

  • The module is named com.example.myapp.
  • It requires two other modules: the standard java.sql and a custom com.example.utils.
  • It exports the package com.example.myapp.api for use by other modules.

The module system uses this information to enforce access rules and dependency correctness at both compile-time and runtime.

Creating Your First Module

Starting a modular project requires organizing your source files into module-specific directories.

Here’s a typical structure for a module named com.example.app:

cpp com.example.app/
├── module-info.java
└── com/
    └── example/
        └── app/
            └── Main.java

The module-info.java might look like this:

java module com.example.app {
    exports com.example.app;
}

This setup indicates that the module com.example.app exports the com.example.app package to other modules.

When compiling, the javac command is module-aware and respects the module boundaries.

bash javac -d out --module-source-path src $(find src -name "*.java")

Running modular applications requires the java command with the --module option:

bash java --module com.example.app/com.example.app.Main

This tells the runtime which module and main class to execute.

Benefits of Using Modules

The advantages I found when adopting modules include:

Strong Encapsulation

Unlike packages, modules prevent unintended access. Only the packages explicitly exported are accessible from outside. This reduces the chance of accidental dependencies and hides internal implementation.

Reliable Configuration

The module system detects missing or conflicting dependencies during compilation and runtime, eliminating many classpath-related issues.

Smaller Runtime Images

Using tools like jlink, I can create custom Java runtime images containing only the modules my application requires. This reduces the application size and improves startup time.

Better Maintainability

Modularization encourages clearer project structure and API design. It’s easier to evolve code without breaking clients since internal packages remain hidden.

Interacting with the Existing Classpath

Migrating legacy code to modules can be challenging. Java provides a transition mechanism called the unnamed module for code on the traditional classpath.

Code in the unnamed module can read all named modules, but named modules cannot read unnamed modules. This means you can use existing libraries without immediately modularizing them but cannot enforce modular boundaries fully.

I started modularizing by moving my code to named modules and leaving third-party libraries on the classpath. Over time, many popular libraries began providing modular JARs, simplifying the transition.

Module Dependencies and Transitive Requires

Modules can specify dependencies using the requires keyword. Sometimes, a module requires another module only because it re-exports some of that module’s API.

For these cases, the transitive modifier comes into play:

java module com.example.api {
    requires transitive com.example.utils;
}

This means any module depending on com.example.api will implicitly depend on com.example.utils. This feature simplifies dependency declarations for API modules.

Services and Service Loaders

Modules support a service provider mechanism, enabling loose coupling between service interfaces and implementations.

A module can declare that it provides an implementation for a service interface:

java module com.example.provider {
    provides com.example.service.MyService with com.example.provider.MyServiceImpl;
}

Consumers can declare that they use a service:

java module com.example.consumer {
    uses com.example.service.MyService;
}

At runtime, ServiceLoader can be used to load implementations dynamically, allowing for flexible extensibility.

I have used this feature to build plugin architectures where new functionality can be added by providing service implementations.

Reflection and Modules

One of the initial concerns about modules was how they would interact with reflection, a core feature in many Java frameworks.

Modules restrict reflective access to only exported packages by default. To allow deep reflection (e.g., for frameworks like Spring or Hibernate), you can use the opens directive:

java module com.example.app {
    opens com.example.app.internal to spring.core;
}

This statement opens the com.example.app.internal package to the spring.core module for reflection, without exporting it generally.

Challenges in Modularizing Large Projects

Despite the benefits, I found several hurdles when modularizing large legacy codebases:

  • Splitting monolithic JARs: Deciding how to break large projects into meaningful modules can be complex.
  • Third-party dependencies: Many libraries were not modularized initially, forcing me to use the unnamed module or create automatic modules.
  • Split packages: Java forbids two modules exporting the same package, so restructuring packages was necessary.
  • Tooling support: Build tools and IDEs initially lagged behind full support for modules, requiring workarounds.

However, these challenges are mostly resolved today with improved tooling and library support.

Best Practices for Using Java Modules

From my experience, here are some tips to get the most out of the Java module system:

  • Design modules around cohesive functionality and clear API boundaries.
  • Export only what is necessary; avoid exposing internal packages.
  • Use requires transitive carefully to manage public dependencies.
  • Modularize incrementally, starting with your own code before third-party libraries.
  • Leverage jlink to create optimized runtime images.
  • Test module boundaries using the jdeps tool to analyze dependencies.

How Modules Affect the Java Platform Itself

One of the major outcomes of Project Jigsaw was modularizing the Java platform itself.

Prior to Java 9, the platform was a monolith. With modules, the JDK is split into smaller modules like java.base, java.sql, java.xml, and many others.

This modular runtime allows developers to include only what is necessary for their application, trimming down resource usage and startup times.

From my perspective, this modularization enhances the portability of Java across devices, including constrained environments.

Real-World Use Cases

Modules shine in scenarios where large codebases need clean separation or where applications must be delivered with optimized runtimes.

For example:

  • Microservices architectures where each service can be a module.
  • Plugin systems that dynamically load modules at runtime.
  • Embedded systems where runtime size is critical.
  • Enterprise applications with well-defined APIs and internal logic encapsulated.

Conclusion

Java Modules introduced by Project Jigsaw represent a milestone in the evolution of Java, bringing clarity, encapsulation, and reliability to the platform. The ability to declare dependencies explicitly and control package visibility enhances both application design and runtime performance.

Although the transition requires planning and effort, especially for existing projects, the benefits far outweigh the initial costs.

By embracing modules, I’ve been able to write more maintainable, scalable, and efficient Java applications and I encourage you to explore this powerful feature for your projects.

Similar Posts