skip to content

During a JPMS migration, what problems arise from automatic modules and split packages, and how do you reason about them?

level: principalimportance: nice to knowfreq 28%

answer

  1. Automatic module = leaky bridge: exports all, reads all (incl. unnamed), unstable name
  2. Don't expose an automatic module's leaked internals in your own API
  3. Split package = same package in two modules -> JPMS rejects it
  4. Causes: multi-JAR libs, JDK package overlap, bad shading
  5. Fix splits: merge / relocate(shade) / replace; scaffolding is transitional

basics

~20 s

Automatic modules read and export everything, so they give no real encapsulation and their derived names can be unstable. JPMS also forbids the same package being supplied by two modules (a split package), which legacy JARs often violate — that causes resolution failures you must resolve by merging or relocating packages.

solid answer

~50 s

Two migration hazards dominate. First, **automatic modules are a leaky bridge**: they implicitly export all packages and read all modules (including the unnamed module), so depending on one gives you a name to `requires` but none of JPMS's encapsulation or reliable-configuration guarantees; worse, a filename-derived name is unstable across renames, so pinning production wiring to it is risky. Second, **split packages**: JPMS requires that any given package be defined in exactly one module that another module reads. Legacy code frequently spreads one package across multiple JARs (or duplicates JDK packages), which JPMS rejects with errors during resolution. The principal-level reasoning is to treat automatic modules as strictly transitional — drive upstreams to publish `Automatic-Module-Name` then real `module-info`, avoid building APIs that lean on an automatic module's leaked internals, and eliminate split packages by merging, relocating (shading), or excluding the offending artifacts. You also plan migration top-down so named app modules can `requires` automatic dependencies while you incrementally harden the graph.

go deeper

for a junior

Aware that automatic modules are a migration helper and that two JARs sharing a package can cause problems.

for a middle

Explains that automatic modules export and read everything and that split packages (same package in two modules) are rejected by JPMS.

for a senior

Diagnoses split-package and automatic-module errors, applies fixes (merge/relocate/replace, move to module path), and avoids depending on leaked internals.

for a principal

Sets migration policy: automatic modules and classpath as transitional scaffolding, demands stable module names from upstream, designs APIs that don't leak automatic-module internals, and systematically eliminates split packages to unlock full JPMS guarantees.

## Setting the scene Large codebases adopt JPMS gradually, leaning on **automatic modules** (plain JARs on the module path) to bridge to not-yet-modular dependencies. That bridge has sharp edges, and a separate structural rule — **no split packages** — trips up legacy code. A principal engineer needs to reason about both and set policy. ## Hazard 1: automatic modules are deliberately leaky Recall what an automatic module *is*: a non-modular JAR given module status so a named module can `requires` it. To make that work without a descriptor, the runtime grants it two blanket permissions: - it **implicitly exports every package** (no encapsulation), and - it **implicitly reads every other module**, including the **unnamed module**. Consequences to reason about: 1. **No encapsulation.** Anything in the automatic module's JAR — including what the library author considers private internals — is exported and reachable. If your code (or a transitive consumer) starts depending on those internals, you've coupled to implementation detail that will *break* the day the library ships a real `module-info` that hides them. So a key rule is: **don't let your public API surface lean on an automatic module's leaked internals.** 2. **Unstable identity.** If the JAR lacks an `Automatic-Module-Name` manifest header, its name is derived from the **filename** (version stripped, non-alphanumerics to dots). Renames, mirror differences, or version-scheme quirks can change that name and break every `requires`. An illegal derived name (e.g. one starting with a digit) makes the JVM reject the JAR outright. 3. **It still sees the classpath.** Because automatic modules read the unnamed module, a half-migrated app keeps working — but this also means you can't rely on the dependency graph being fully validated; the automatic module can pull in anonymous classpath code that isn't represented in the module graph. **Policy stance:** treat automatic modules as a *temporary* state. Prefer dependencies that publish `Automatic-Module-Name` (stable name) and push toward those that publish a real `module-info` (true encapsulation). Where neither exists and the risk is high, repackage/shade the dependency under your own coordinates so you control its module identity and contents. ## Hazard 2: split packages JPMS enforces a structural invariant: **a given package may be defined in only one module** that a particular module reads. Two modules in the same readability scope contributing the *same* package is a **split package**, and the runtime rejects it. Why the rule exists: if package `com.foo` lived in two modules, the system couldn't unambiguously decide which module's classes you get, and the *reliable configuration* guarantee would collapse. So JPMS disallows it by construction. Where it bites in legacy code: - A library historically shipped as several JARs that all contribute classes to the same package (common before modularisation). - An old JAR redefines a package that the JDK itself now owns (e.g. some XML/`java.*`/`javax.*` packages that moved into platform modules). On the module path this collides with the platform module; you cannot just `requires` your way out. - 'Uber' shading that merges packages can also create or hide splits depending on how it's done. The errors surface during resolution (e.g. *'module X reads package P from both Y and Z'* or *'package P in both module A and the unnamed module'*). ## Reasoning about fixes (split packages) - **Merge** the split: combine the JARs that share a package into one module so the package has a single owner. - **Relocate (shade):** rename the offending package to a unique prefix at build time so it no longer collides. - **Exclude/replace:** drop the legacy JAR in favour of a maintained, modular replacement, or exclude the duplicated JDK-overlap package. - **As a last resort during transition:** keep the colliding pieces on the classpath (unnamed module) where the no-split rule isn't enforced — but accept you've then given up modular guarantees for them, and named modules still can't read them. ## Putting it together (the principal view) The through-line is that **automatic modules and the classpath are scaffolding, not a destination.** A sound migration: modularise top-down; depend on automatic modules only transitionally; never expose their leaked internals in your own APIs; demand stable `Automatic-Module-Name` from upstreams; and proactively hunt and eliminate split packages (merge/relocate/replace) so the module graph can be fully validated. The payoff is the JPMS guarantees the scaffolding was hiding: explicit, validated dependencies and genuine encapsulation.

  • Why does JPMS forbid split packages at all?
    Because a package owned by two readable modules makes class resolution ambiguous (which module's class do you load?), which would break the reliable-configuration guarantee; so the system requires each package to have exactly one owning module in a given readability scope.
  • Your team depends on an automatic module and someone starts calling a class the library considers internal. What's the long-term risk and your guidance?
    When the library ships a real module-info it will stop exporting that package, so the call breaks at upgrade time. Guidance: forbid depending on automatic-module internals in code review, wrap any needed access behind your own adapter, and track the upstream's modularisation so you can switch to the supported API.
  • How can you keep a half-migrated app working when a split package involves a JDK platform package?
    Relocate/exclude the legacy package that overlaps the platform module (e.g. via shading), replace the artifact with a modular one, or keep the offending JAR on the classpath as a stopgap — accepting it loses modular guarantees and stays invisible to named modules.

saying these in an interview costs you the question

  • Treating automatic modules as a permanent solution rather than transitional scaffolding
  • Assuming automatic modules give encapsulation — they export everything
  • Thinking split packages are allowed if the classes differ — the package name collision alone is rejected
  • Believing you can fix a split by adding more requires — you must merge/relocate/replace
  • Pinning production requires to a filename-derived automatic module name

context