Java Platform Module System (JPMS)
15 questions found
What is the Java Platform Module System (JPMS, introduced in Java 9), and what core problem does it aim to solve compared to the traditional classpath-based approach?
Beginner
JPMS introduces explicit, JVM-enforced modules (declared via a module-info.java file) as a higher-level organizational unit above individual packages/JARs, letting you explicitly declare a module's dependencies (requires) and what it exposes to other modules (exports) -- this solves several longstanding classpath-era problems: the lack of genuine encapsulation (any public class on the classpath was effectively visible to any other code, regardless of intended internal-only status), no explicit, verifiable dependency declarations (you couldn't easily tell what a JAR actually depended on without inspecting its code), and the notorious 'JAR hell' (multiple JARs on the classpath silently conflicting or providing incompatible versions of the same classes with no clear detection or resolution mechanism).
// module-info.java
module com.example.myapp {
requires java.sql; // explicit dependency declaration
requires com.example.shared;
exports com.example.myapp.api; // only this package is visible to other modules
// com.example.myapp.internal is NOT exported -- genuinely, JVM-enforced hidden
}
Real-world example
A large enterprise application modularizes its previously monolithic classpath-based codebase into explicit JPMS modules, immediately surfacing several previously-invisible implicit dependencies between components that had been silently relying on classpath-wide visibility, forcing the team to make these dependencies explicit and properly declared.
Common follow-ups: How does JPMS's module-level encapsulation differ from and complement the class-level access modifiers (private, protected) that already existed before Java 9?;What is the 'unnamed module', and how does traditional classpath-based code interact with the module system?
Class Loading & Bytecode Verification;Build Tools: Maven & Gradle
How do the requires and exports directives in a module-info.java file work together to define a module's dependencies and public API surface?
Intermediate
requires ModuleName declares that your module depends on another named module, making that dependency's exported packages available to your module's own code (and causing a compile/runtime error if the required module isn't present, rather than a classpath-era silent NoClassDefFoundError potentially discovered much later); exports package.name declares that a specific package within YOUR module is accessible to OTHER modules that require yours, while any package NOT explicitly exported remains genuinely inaccessible outside your module (not just conventionally private, but JVM-enforced, rejecting even reflective access attempts by default) -- together these two directives let a module precisely control both what it needs and what it exposes.
// module-info.java for a library module
module com.example.mylibrary {
requires java.logging; // this library itself depends on java.logging
exports com.example.mylibrary.api; // public API, visible to consuming modules
// com.example.mylibrary.internal.impl is NOT exported -- truly hidden implementation detail
}
// A consuming module
module com.example.consumer {
requires com.example.mylibrary; // can now use com.example.mylibrary.api.* classes
// but CANNOT access com.example.mylibrary.internal.impl.* at all -- compile error if attempted
}
Real-world example
A library author confidently refactors internal implementation classes in a non-exported package, knowing with certainty (enforced by the JVM itself, not just a naming convention) that no external consumer could possibly have compiled against or be relying on those specific classes, since JPMS genuinely prevents that access rather than merely discouraging it.
Common follow-ups: What happens if module A requires module B, but module B doesn't actually export the specific package module A tries to use?;How does 'requires transitive' differ from a plain 'requires' declaration?
OOP & Classes;Design Patterns in Java
What is the difference between 'requires' and 'requires transitive', and how does transitive dependency propagation work in the module system?
Advanced
A plain requires ModuleName gives your module access to that dependency's exported packages, but does NOT automatically propagate that access to modules that in turn require YOUR module (they'd need their own separate requires declaration for that same dependency if they need to use it directly); requires transitive ModuleName additionally propagates that dependency to any module requiring yours, meaning if module C requires module B (transitively), and module B requires transitive module A, then module C automatically also gains access to module A's exports without needing its own explicit requires A declaration -- this is specifically useful when your module's own public API directly exposes types from a dependency (like a method returning a type from that dependency), since consumers of your API genuinely need access to that dependency's types too, not just your own module's exports.
// module A (a shared library)
module com.example.geometry { exports com.example.geometry; }
// module B, whose PUBLIC API exposes types from module A
module com.example.shapes {
requires transitive com.example.geometry; // propagates geometry access to consumers of shapes
exports com.example.shapes; // shapes' public API includes methods returning geometry types
}
// module C, using shapes
module com.example.app {
requires com.example.shapes;
// Automatically ALSO has access to com.example.geometry's exports, via transitive propagation,
// without needing 'requires com.example.geometry' explicitly here
}
Real-world example
A shapes library module whose public API methods return types from a separate geometry module uses requires transitive for its geometry dependency, ensuring any consumer of the shapes module automatically gains the necessary access to work with those returned geometry types, without each individual consumer needing to separately discover and declare that same underlying dependency themselves.
Common follow-ups: What would go wrong (a compile error) for a consumer module if 'requires transitive' were used instead of a plain 'requires' in this exact scenario?;How do you decide whether a given dependency should be declared as plain requires or requires transitive?
Design Patterns in Java;Build Tools: Maven & Gradle
What is the difference between 'exports' and 'opens' in a module-info.java file, and why does reflection-heavy code (like many dependency injection or serialization frameworks) specifically require the latter?
Intermediate
exports makes a package's public types accessible to other modules at COMPILE time and normal runtime method calls, but does NOT permit deep reflective access (like calling setAccessible(true) to bypass normal access control checks on private members) from outside the module; opens specifically permits this deep reflective access at runtime (needed by frameworks like Spring, Hibernate, or Jackson that use reflection to inspect and manipulate even PRIVATE fields/constructors of your classes, a common technique for dependency injection, ORM mapping, or JSON deserialization) -- a package can be both exported AND opened if it needs regular compile-time access as well as deep reflective access, or a module can use 'opens' without 'exports' if it needs to permit reflection but doesn't want to expose the package for normal compile-time linking.
// module-info.java
module com.example.myapp {
requires com.fasterxml.jackson.databind;
exports com.example.myapp.api; // normal compile-time access for consumers
opens com.example.myapp.model to com.fasterxml.jackson.databind; // deep reflection specifically for Jackson
}
// Without 'opens', Jackson's attempt to reflectively access private fields on
// classes in com.example.myapp.model would throw an InaccessibleObjectException at RUNTIME
Real-world example
A Spring Boot application using JPA entities in a non-exported package encounters InaccessibleObjectException at runtime when Hibernate attempts reflective field access for ORM mapping, resolved by adding an explicit 'opens' directive for that specific package to Hibernate's module, granting the deep reflective access the ORM framework genuinely needs.
Common follow-ups: Why did the module system designers create this distinction rather than just having a single access-granting directive?;What is 'opens ... to' with a specific target module, versus a plain unqualified 'opens' -- what's the security trade-off between them?
Reflection API;Serialization & Deserialization
How does the 'unnamed module' work, and how does JPMS maintain backward compatibility with the vast ecosystem of pre-existing JARs that were never designed with modules in mind?
Advanced
Any code placed on the traditional classpath (rather than the module path) is automatically treated as part of a special 'unnamed module', which is granted implicit access to read every other named module's exports (a pragmatic, deliberately permissive compatibility measure), and conversely, code in named modules can be configured to read the unnamed module too -- this design lets pre-existing, non-modularized JARs continue working essentially unchanged when placed on the classpath, providing a gradual migration path where an application can mix explicitly modularized code with legacy classpath-based dependencies during a transition period, rather than requiring an all-or-nothing simultaneous migration of an entire dependency tree.
# Running with a MIX of the module path (explicitly modularized code) and classpath (legacy JARs)
java --module-path mymodules --add-modules com.example.myapp \
--class-path legacy-lib.jar \
-m com.example.myapp/com.example.myapp.Main
# legacy-lib.jar (unnamed module) can freely use any named module's exports,
# and com.example.myapp can be configured to use legacy-lib.jar's classes too
Real-world example
A large legacy application incrementally adopts JPMS by modularizing its own core application code first while leaving several older, unmaintained third-party dependencies on the traditional classpath (as part of the unnamed module), achieving partial modularization benefits without needing to wait for or force those legacy dependencies to be updated with proper module declarations first.
Common follow-ups: What are 'automatic modules', and how do they provide an intermediate step between the unnamed module and a fully-declared named module for a JAR without a module-info.java?;What specific compatibility risks or limitations remain even with the unnamed module's permissive design?
Class Loading & Bytecode Verification;Build Tools: Maven & Gradle
What is an automatic module, and how does placing a plain (non-modularized) JAR on the module path (rather than the classpath) differ from leaving it on the classpath as part of the unnamed module?
Intermediate
An automatic module is created automatically by the JVM when a plain JAR (lacking its own module-info.java) is placed on the MODULE path rather than the classpath -- the JVM derives the automatic module's name from the JAR's filename (or an Automatic-Module-Name manifest entry if present, providing a more stable, intentional name than filename-derivation would), and grants it broad, permissive access (it reads every other module, and exports/opens ALL of its own packages unconditionally) -- this differs from the unnamed-module/classpath approach specifically in that OTHER named modules can now explicitly 'requires' this automatic module by its derived name, participating more fully in the module graph, which the pure unnamed-module approach doesn't support (named modules generally cannot 'requires' the unnamed module directly).
# A plain, non-modularized library JAR placed on the MODULE path becomes an automatic module
java --module-path mymodules:legacy-lib.jar --module com.example.myapp
// Now other named modules CAN explicitly declare a dependency on it
module com.example.myapp {
requires legacy.lib; // name derived from the JAR's filename, or its Automatic-Module-Name manifest entry
}
Real-world example
A team migrating toward full JPMS modularization places a legacy, non-modularized dependency JAR on the module path (as an automatic module) rather than the classpath, letting their own newly-modularized code explicitly declare and document a 'requires legacy.lib' dependency, an intermediate step providing more structure than leaving it on the classpath while the legacy JAR itself hasn't yet been properly modularized.
Common follow-ups: Why is relying on filename-derived automatic module names considered fragile, and how does Automatic-Module-Name in a JAR's manifest address this?;What's the migration path from a JAR being an automatic module to eventually becoming a fully explicit named module?
Build Tools: Maven & Gradle;Class Loading & Bytecode Verification
How would you use jlink to create a custom, minimal Java runtime image containing only the specific modules an application actually needs, and what deployment benefits does this provide?
Advanced
jlink (a JDK tool) assembles a custom runtime image by analyzing an application's module dependencies and including ONLY those specific JDK modules (plus the application's own modules) actually required, rather than bundling the entire JDK -- this produces a significantly smaller, self-contained deployable runtime image (potentially reducing the deployed footprint from hundreds of megabytes for a full JDK down to a much smaller custom image containing just what's genuinely needed), particularly valuable for containerized deployments (smaller container images, faster pull/startup times) or resource-constrained environments, and since the custom image is entirely self-contained, it doesn't even require a separately-installed JDK/JRE on the target deployment machine at all.
# Create a custom runtime image containing only the required modules
jlink --module-path $JAVA_HOME/jmods:mymodules \
--add-modules com.example.myapp \
--output custom-runtime \
--strip-debug --compress=2 --no-header-files --no-man-pages
# Run the application using ONLY the custom, minimal runtime image -- no separate JDK install needed
./custom-runtime/bin/java --module com.example.myapp/com.example.myapp.Main
Real-world example
A containerized microservice reduces its Docker image size dramatically by using jlink to bundle only the specific JDK modules its code actually depends on (like java.base and java.sql, but not, for example, java.desktop or other unused modules), resulting in significantly faster image pulls and container startup times compared to shipping alongside a full, general-purpose JDK installation.
Common follow-ups: How does jlink determine exactly which JDK modules an application transitively depends on?;What are the deployment and maintenance trade-offs of a custom jlink-produced runtime image versus relying on a standard, widely-available JDK installation?
Deploy Checklist;Diagnostics & Performance
How does the ServiceLoader/Service Provider Interface pattern integrate with JPMS using the 'uses' and 'provides ... with' directives, replacing the older META-INF/services file-based mechanism?
Intermediate
Under JPMS, a module declares that it CONSUMES a service via uses ServiceInterface (letting it use ServiceLoader.load() to discover implementations), and a module PROVIDING an implementation declares provides ServiceInterface with ImplementationClass -- this JPMS-native declaration mechanism replaces (though doesn't strictly require replacing, for backward compatibility) the older convention-based META-INF/services/ text file approach, giving the module system full, explicit visibility into service provider relationships as part of the formal module graph, rather than relying on a loosely-coupled convention the module system itself has no direct awareness of.
// Consumer module
module com.example.app {
uses com.example.spi.PaymentProcessor; // declares it will consume this service
}
// Provider module
module com.example.stripe.provider {
provides com.example.spi.PaymentProcessor with com.example.stripe.StripePaymentProcessor;
}
// Consumer code, unchanged from the pre-JPMS pattern
ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
Real-world example
A plugin-based application declares 'uses com.example.spi.Plugin' in its own module-info.java, and each separately-developed and deployed plugin module declares 'provides com.example.spi.Plugin with ...', giving the module system explicit, verifiable knowledge of every plugin relationship in the application, rather than relying purely on the older convention-based text file mechanism the module system has no direct visibility into.
Common follow-ups: Does the older META-INF/services file-based mechanism still work at all under JPMS, or is the module-info-based declaration strictly required?;How does this explicit service declaration interact with automatic modules, which lack their own module-info.java?
Design Patterns in Java;Class Loading & Bytecode Verification
How would you diagnose and resolve common JPMS migration errors, such as 'package is not visible' or a split package conflict, when modularizing an existing large application?
Advanced
'Package is not visible' errors typically indicate an attempt to access a package that exists in a required module but wasn't actually exported (or wasn't opened, for reflective access) by that module, resolved by either adding the appropriate exports/opens directive to the PROVIDING module (if you control it) or, if it's a third-party dependency you don't control, potentially needing to fall back to the classpath/unnamed-module approach for that specific dependency; split package conflicts (as covered earlier) occur when two modules on the module path both contain the same package name, resolved by keeping the conflicting JARs on the classpath instead, or by working with the affected library maintainers to properly separate the overlapping packages -- a broader, pragmatic migration strategy for large existing applications commonly involves an incremental approach: modularizing top-level application code first while leaving less-critical or harder-to-migrate dependencies on the classpath temporarily, gradually shrinking that classpath-based portion over time as dependencies get updated to properly support modules.
# Common error encountered during JPMS migration:
# java.lang.IllegalAccessError: class com.example.app.Main cannot access class
# com.example.lib.internal.Helper (in module com.example.lib) because module
# com.example.lib does not export com.example.lib.internal to module com.example.app
# Resolution: either the lib module adds 'exports com.example.lib.internal to com.example.app;'
# (a QUALIFIED export, limiting visibility to just this specific consumer),
# or the consuming code is refactored to avoid needing that internal package entirely
Real-world example
A team modularizing a large legacy application encounters dozens of 'package is not visible' errors during their initial migration attempt, tracing most of them to internal utility packages that application code had been (perhaps unintentionally) relying on despite those packages never being part of the library's genuinely intended public API, using this migration process itself as a valuable forcing function to properly identify and clean up such unintended internal-API dependencies.
Common follow-ups: What's a qualified export ('exports package to specificModule'), and when would you use it instead of a plain unqualified export?;What incremental migration strategies help make a large-scale JPMS adoption more manageable than an all-at-once approach?
Class Loading & Bytecode Verification;Diagnostics & Performance
How would you write and run a basic 'Hello World' JPMS-modularized application from scratch, including the module-info.java, directory structure, and the specific javac/java command-line invocations needed?
Intermediate
A modularized application requires: a module-info.java file at the root of the module's source directory declaring the module (module com.example.hello { }), Java source files organized in a directory structure matching their package names beneath that root, compilation using javac with --module-source-path (or a simpler direct approach for a single module), and execution using java with --module-path pointing at the compiled module's location plus --module (or -m) specifying which module and main class to run -- this end-to-end process, while more verbose than the traditional classpath-based javac/java invocation for a single unmodularized class, becomes second nature once the basic module directory/compilation/execution pattern is understood.
// Directory structure:
// src/com.example.hello/module-info.java
// src/com.example.hello/com/example/hello/Main.java
// module-info.java
module com.example.hello {
}
// Main.java
package com.example.hello;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, modular world!");
}
}
# Compile
javac -d out --module-source-path src $(find src -name "*.java")
# Run
java --module-path out --module com.example.hello/com.example.hello.Main
Real-world example
A developer learning JPMS for the first time works through this exact basic 'Hello World' modularized example, getting hands-on familiarity with the module-info.java syntax and the specific --module-source-path/--module-path/--module command-line flags before attempting to tackle modularizing a genuinely large, complex existing application.
Common follow-ups: How does this command-line compilation/execution process differ when using a build tool like Maven or Gradle instead of invoking javac/java directly?;What does the module-info.java file look like for a module that both requires other modules AND exports/provides its own functionality?
Build Tools: Maven & Gradle;Java Fundamentals: Syntax
Data Types & Operators