Packages are excellent for organizing Java classes and controlling access within an application, but large applications eventually face a bigger challenge: managing dependencies and defining stronger boundaries between groups of related code. Java modules were introduced to address this problem.
A module can be thought of as a larger organizational unit built on top of packages. While a package groups related Java types, a module groups related packages and explicitly declares which dependencies it needs and which packages it makes available to other modules.
Why Were Java Modules Introduced?
As Java applications become larger, simply organizing classes into packages is not always enough. A large application may contain hundreds of packages and thousands of classes, with dependencies spreading across many parts of the system.
Without explicit module boundaries, it can become difficult to answer questions such as:
- Which libraries does this component depend on?
- Which packages are intended for other applications to use?
- Which internal packages should remain hidden?
- Which modules are required for this application to run?
The Java Platform Module System, introduced in Java 9, provides a formal mechanism for answering these questions.
Important: Java modules were introduced in Java 9. They provide stronger dependency management and encapsulation at a level above packages.
Package vs Module
The easiest way to understand modules is to compare them with packages.
| Feature | Package | Module |
|---|---|---|
| Groups | Classes and related types | Packages |
| Main purpose | Organization and namespace | Dependency management and strong encapsulation |
| Introduced | Early Java | Java 9 |
| Descriptor | No required module descriptor | Uses module-info.java |
| Dependency declaration | Not provided at package level | Declared using requires |
| API exposure | Controlled mainly through access modifiers | Packages can be explicitly exported |
What Is a Module?
A module is a named collection of packages and resources with a formal description of its dependencies and the packages it exposes to other modules.
For example, an application might contain:
com.example.user com.example.order com.example.payment
These packages could be grouped into a module such as:
com.example.shop
The module can then declare which other modules it requires and which of its packages are part of its public API.
Real-World Analogy
Think about a large office building. Individual rooms are similar to classes, departments are similar to packages, and an entire secured section of the building can be compared to a module.
A department may contain many rooms, but the security system can decide which departments have access to which secured sections. Similarly, a module can contain multiple packages while explicitly controlling dependencies and package visibility.
Java Module Example
Suppose we create a module named com.example.library. Its structure might look like this:
src/
└── com.example.library/
├── module-info.java
└── com/
└── example/
└── library/
├── Book.java
└── LibraryService.java
The module descriptor defines the module's identity and boundaries.
module com.example.library {
}
This is the simplest possible module declaration. It identifies the module but does not yet declare dependencies or exported packages.
Modules Group Packages
A module can contain several packages.
module com.example.shop {
}
For example, the module could contain:
com.example.shop.product com.example.shop.order com.example.shop.payment com.example.shop.internal
The module can then decide which packages should be exposed to other modules.
Module Dependencies
One of the most useful features of modules is explicit dependency declaration. A module can state which other modules it requires.
module com.example.application {
requires com.example.library;
}
This tells Java that com.example.application depends on the module com.example.library.
The dependency becomes part of the module's declared structure instead of being hidden among source-code imports and runtime configuration.
Exporting Packages
A module can contain packages that should remain internal and packages that should be accessible to other modules. The exports directive identifies packages that form part of the module's API.
module com.example.library {
exports com.example.library.api;
}
Other modules can access public types from the exported package, subject to the module system and normal Java access rules.
Unexported Packages
A package that is not exported can remain internal to the module.
module com.example.library {
exports com.example.library.api;
}
Suppose the module also contains:
com.example.library.api com.example.library.internal
Only the api package is exported. The internal package can therefore remain hidden from ordinary access by other modules.
Remember: A public class inside a non-exported package is not automatically available to every other module. Module boundaries add another layer of access control beyond Java's normal access modifiers.
Modules and Strong Encapsulation
One of the major benefits of the module system is stronger encapsulation. Traditional Java access modifiers control visibility at the class and member level, while modules can control visibility at the package level.
This allows a library to expose a carefully designed public API while keeping implementation packages hidden.
module com.example.payment {
exports com.example.payment.api;
}
A consumer can use the API package without being encouraged to depend directly on internal implementation packages.
Requires and Exports Together
A realistic module descriptor may contain both dependencies and exported packages.
module com.example.application {
requires com.example.library;
exports com.example.application.api;
}
Here, the module requires another module while exposing one of its own packages.
Unnamed and Automatic Modules
Java's module system was designed to support migration from older applications that were created before Java 9. Not every existing Java library immediately became a named module.
This led to concepts such as the unnamed module and automatic modules, which help traditional classpath-based applications and libraries interact with the module system during migration.
For beginners, the key idea is simple: modern modular applications can use explicit named modules, while older classpath-oriented code can still operate without immediately becoming fully modular.
Module Path vs Classpath
Traditional Java applications commonly use the classpath to locate classes and libraries. Modular applications use the module path to locate modules.
| Feature | Classpath | Module Path |
|---|---|---|
| Primary unit | Classes and JAR files | Modules |
| Dependency declaration | Generally implicit or externally configured | Declared in module descriptors |
| Package visibility | Mainly controlled by Java access modifiers | Also controlled by module exports |
| Typical use | Traditional Java applications | Modular Java applications |
Common Benefits of Java Modules
- Explicitly declares dependencies between modules.
- Provides stronger encapsulation of packages.
- Creates clearer application boundaries.
- Can improve maintainability in large applications.
- Supports more reliable dependency analysis.
- Provides the foundation for modular Java applications.
Common Beginner Mistakes
- Thinking that a module is simply another name for a package.
- Assuming every public class is automatically accessible from another module.
- Forgetting that required modules must be declared using requires.
- Assuming that every package inside a module is automatically exported.
- Confusing the classpath with the module path.
- Creating modules without a clear dependency or encapsulation strategy.
Best Practices
- Keep module boundaries meaningful rather than creating many tiny modules without a clear reason.
- Export only packages that form part of the module's intended API.
- Declare dependencies explicitly with requires.
- Keep implementation packages unexported when external access is unnecessary.
- Design module boundaries around clear responsibilities and stable APIs.
Interview Insight
A common interview question is, "What is the difference between a package and a module?" A strong answer is: A package organizes related Java types, while a module is a higher-level unit that groups packages and explicitly defines dependencies and which packages are accessible to other modules.
Interview Tip: Remember the hierarchy concept: classes belong to packages, and packages can belong to modules. Packages primarily organize types, while modules provide stronger dependency management and encapsulation.
Quick Revision
| Concept | Key Point |
|---|---|
| Module | A named collection of packages with explicit dependencies and boundaries. |
| Java 9 | Introduced the Java Platform Module System. |
| requires | Declares a dependency on another module. |
| exports | Makes a package available to other modules. |
| Encapsulation | Modules can hide packages that are not part of their public API. |
| Module path | Used to locate modules for modular applications. |
| Package relationship | A module can contain multiple packages. |
Java modules take the organizational ideas of packages and extend them to a larger architectural boundary. They make dependencies explicit and allow applications to expose only the packages that are intended to form a public API. The next chapter focuses on module-info.java, the file that defines these module-level rules.
