Java 9 Module System (JPMS) — Motivation, module-info.java, and Migration
Fast recall
Quick revision
Core answer
module, exports, requires, opens, uses, and provides. The motivation: before Java 9, the JDK was a monolithic 60+MB rt.jar with no enforced boundaries — any class could access any other via reflection, JARs on the classpath shadowed each other silently (the 'classpath hell'), and you couldn't ship a slim runtime. JPMS enables strong encapsulation (packages not exported are truly inaccessible, even via reflection unless opens), reliable configuration (missing dependencies fail at startup, not at first use), and scalable runtime images via jlink (ship only the modules your app needs — as small as ~25MB vs 200MB+ full JDK). Migration challenges: most existing code runs unmodified on the classpath (the 'unnamed module'), but libraries using deep reflection (Spring, Hibernate, Jackson) hit InaccessibleObjectException on internal JDK classes — fixed via --add-opens JVM flags or explicit opens in module-info. The split-package problem (two JARs contributing to the same package) breaks under JPMS. Automatic modules (non-modular JARs on the module path) ease incremental migration. Adoption outside the JDK itself is slow; most libraries are still classpath-friendly. For typical Spring Boot apps, you usually don't write module-info.java — the JDK is modular internally, but your app runs on the classpath.Whether the candidate knows JPMS's motivation (strong encapsulation, reliable configuration, jlink images), the module-info syntax, and the pragmatic reality that most apps don't modularize but benefit indirectly via jlink
Thinking JPMS replaces Maven/Gradle dependency management (it doesn't — it's runtime, they're build-time). Thinking every app should have module-info.java. Confusing `exports` (compile + run) with `opens` (reflection only)
Mention jlink custom runtime images and measurable size reductions, explain the automatic module vs unnamed module vs explicit module trilemma, discuss why Spring Boot apps mostly stay on the classpath, and reference `--add-opens` as the practical migration escape hatch
Interview-ready guide
Interview answer
The Java Platform Module System, delivered in Java 9, adds a module layer above packages that controls which classes are exported to other modules and which other modules are required.
Why it was added
- Monolithic JDK —
rt.jarwas 60MB of everything; couldn't ship a slim runtime - Classpath hell — duplicate classes silently shadowed each other; missing deps failed at first use
- No strong encapsulation —
sun.misc.Unsafeand all JDK internals were reachable; libraries depended on non-stable internals - Goal: strong encapsulation + reliable configuration + custom runtime images
`module-info.java` at the module root declares:
requires X— I need module Xrequires transitive X— my consumers also get Xrequires static X— compile-time only (annotation processors like Lombok)exports pkg— package visible to everyoneexports pkg to A, B— qualified; only A and Bopens pkg— allow deep reflection (Spring, Hibernate, Jackson need this)uses Service/provides Service with Impl— ServiceLoader
`exports` vs `opens` — the crucial distinction
exports= compile and runtime access to public APIopens= full reflection access including private fields- Frameworks that use reflection need
opens, not justexports
Three kinds of modules
- Named module — has module-info, full JPMS enforcement
- Automatic module — plain JAR on the module path; auto-exports and auto-requires everything; bridge for non-modular libraries
- Unnamed module — JAR on the classpath; no enforcement; where most app code still lives
`jlink` — the killer feature
- Build a custom JDK with only the modules your app uses
jdeps --print-module-depsto discover them- Typical reduction: 200MB full JDK → 30–90MB custom image
- Matters for Docker images, AWS Lambda, embedded devices
- Works even if your app isn't modular, because the JDK itself is
Migration challenges
- `InaccessibleObjectException` when frameworks reflect into JDK internals
- Fix with `--add-opens java.base/java.util=ALL-UNNAMED` at startup — the standard escape hatch
- Removed modules: Java EE bits (JAXB, JAX-WS, CORBA) now external deps
- Split packages forbidden on module path;
jdepsfinds them - Most libraries only add
Automatic-Module-Name: com.acme.footo manifest — the minimum useful step
Adoption reality
- The JDK is fully modular internally
- Most Spring Boot apps stay on the classpath — they benefit from
jlinkwithout modularizing themselves - Library authors add
Automatic-Module-Nameat minimum - Greenfield libraries: worth full modularization
- Legacy migrations: usually not worth the friction
Recommendation: For typical Spring Boot microservices, don't modularize your own code — the friction with Spring/Hibernate/Jackson reflection is rarely worth it. Do benefit indirectly by using `jlink` to build slim JDK images for Docker and Lambda — a 180MB full JDK shrinks to 45MB for a typical app, dramatically reducing container pull times and cold-start costs. If you publish a library, add `Automatic-Module-Name: com.acme.yourlib` to the manifest as the minimum courtesy to downstream consumers — it's free. Keep `--add-opens` flags in your JAVA_TOOL_OPTIONS or Dockerfile as a migration escape hatch for frameworks that still touch JDK internals. For greenfield projects where you control the whole dependency tree and care about strong encapsulation, full modularization with module-info.java is worth considering — otherwise stay on the classpath.
Java 9 Module System (JPMS) — Motivation, `module-info.java`, and Migration
Direct Answer
The Java Platform Module System (JPMS), delivered in Java 9 via JEP 261/376, adds a module layer above packages that controls which classes are exported to other modules and which other modules are required. Each module declares its contract in module-info.java at the module root, using keywords like module, exports, requires, opens, uses, and provides. The motivation: before Java 9, the JDK was a monolithic 60+MB rt.jar with no enforced boundaries — any class could access any other via reflection, JARs on the classpath shadowed each other silently (the 'classpath hell'), and you couldn't ship a slim runtime. JPMS enables strong encapsulation (packages not exported are truly inaccessible, even via reflection unless opens), reliable configuration (missing dependencies fail at startup, not at first use), and scalable runtime images via jlink (ship only the modules your app needs — as small as ~25MB vs 200MB+ full JDK). Migration challenges: most existing code runs unmodified on the classpath (the 'unnamed module'), but libraries using deep reflection (Spring, Hibernate, Jackson) hit InaccessibleObjectException on internal JDK classes — fixed via --add-opens JVM flags or explicit opens in module-info. The split-package problem (two JARs contributing to the same package) breaks under JPMS. Automatic modules (non-modular JARs on the module path) ease incremental migration. Adoption outside the JDK itself is slow; most libraries are still classpath-friendly. For typical Spring Boot apps, you usually don't write module-info.java — the JDK is modular internally, but your app runs on the classpath.
Common Mistakes & Pitfalls
Thinking JPMS replaces Maven/Gradle dependency management (it doesn't — it's runtime, they're build-time). Thinking every app should have module-info.java. Confusing exports (compile + run) with opens (reflection only)
Check your understanding
Answer these without looking back at the article.
- What's the difference between `exports` and `opens`?
- How does `jlink` produce a smaller runtime than a full JDK?
- Why do Spring/Hibernate apps often need `--add-opens` flags?
- What's an automatic module and when would you use `Automatic-Module-Name`?
- Why was JAXB removed from the JDK in Java 11?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.