skip to content

What is a split package in JPMS, why is it forbidden, and how do you fix one during migration?

level: middleimportance: must knowfreq 62%

answer

  1. Same package in two modules = forbidden
  2. JPMS needs exactly one owner per package (reliable configuration)
  3. Classpath/unnamed module has NO split check
  4. Fix: merge / exclude / relocate
  5. --patch-module is a transition crutch

basics

~20 s

A split package is when the same package name lives in two different modules. The module system forbids it because each package must belong to exactly one module. You fix it by merging the package into one module or moving the classes so the package exists in only one place.

solid answer

~40 s

A split package is when the same package (e.g. javax.annotation) is supplied by two or more modules on the module path. JPMS requires every package to be owned by exactly one module so the runtime can unambiguously resolve which module exports a given type; a split breaks resolution and the module graph fails to build. Typical causes are overlapping legacy jars (old JSR artifacts) or a package straddling a boundary you are drawing. Fixes: merge the conflicting jars into one module, pick one canonical artifact and exclude the duplicate, relocate classes so the package is whole in a single module, or keep the offenders on the classpath (the unnamed module has no split-package check). The escape hatch --patch-module can splice extra classes into a module, but the clean fix is consolidating ownership.

go deeper

for a junior

Can state that two modules cannot contain the same package and that the build fails if they do.

for a middle

Explains the reliable-configuration rationale, identifies common causes (legacy javax jars), and applies merge/exclude/relocate fixes.

for a senior

Weighs trade-offs between classpath migration, dependency exclusion, and --patch-module; recognizes split packages in transitive dependency graphs and resolves them at the build-tool level.

for a principal

Designs module boundaries to avoid splits across an org's library set, defines policy for legacy javax artifacts, and guides large-scale migrations so package ownership is unambiguous before modules are introduced.

## Background: modules and packages A **package** in Java is a namespace for classes, like `com.acme.util`. The **Java Platform Module System (JPMS)**, added in Java 9 (Project Jigsaw), introduces a layer above packages: a **module** is a named, self-describing unit declared by a `module-info.java` file that states what it `requires` (depends on) and `exports` (makes visible). Modules placed on the **module path** form a **module graph** that the runtime resolves at startup. ## What a split package is A **split package** exists when the *exact same package name* is contributed by **two or more modules** at the same time. Example: both `module-a` and `module-b` contain classes in package `javax.annotation`. ## Why JPMS forbids it JPMS guarantees **reliable configuration**: for any package, there is exactly one module that owns and can export it, so the runtime knows unambiguously where a type comes from. If two modules both claimed `javax.annotation`, the resolver could not decide which one a `requires` should read, and **strong encapsulation** (the rule that you can only see exported packages of modules you require) would be undefined. So the module system rejects the configuration at resolution time with an error such as `module X reads package javax.annotation from both Y and Z` or refuses to define two modules owning the same package. Note the contrast with the **classpath / unnamed module**: the classpath has *no* split-package check (it just uses first-wins class loading), which is one reason classpath apps tolerated overlapping jars for years. ## How splits arise during migration 1. **Legacy JSR jars** — many old API jars (annotations, JAXB, JAX-WS) re-declare `javax.*` packages that also exist elsewhere. 2. **Two versions of the same library** transitively pulled in. 3. **Drawing a module boundary** through a package — putting half of `com.acme.util` in one module and half in another. ## How to fix it - **Consolidate ownership** — merge the conflicting jars/classes so the package lives in exactly one module. - **Exclude the duplicate** — choose one canonical artifact (build-tool dependency exclusion) and drop the other. - **Relocate** — move classes so each package is whole in a single module (sometimes renaming the package). - **Stay on the classpath** — leaving the offending jars on the classpath (not the module path) sidesteps the check entirely during incremental migration. - **`--patch-module <module>=<jar/dir>`** — an escape hatch that injects extra classes into a named module at compile/run time, effectively healing a split for a known module; useful as a transition crutch, not a permanent design. ## Key takeaways Split packages are a *configuration* error, not a runtime crash deep in your code: the JVM refuses to start (or javac refuses to compile) when the module path has them. The durable fix is making each package owned by one module; the classpath and `--patch-module` are tactical workarounds during migration.

  • Why does the classpath tolerate split packages but the module path does not?
    The classpath loads everything into the single unnamed module with first-wins class resolution and performs no split-package validation. The module path enforces reliable configuration, requiring each package to be owned by exactly one module, and fails resolution if two modules contribute the same package.
  • Can two modules export different packages that share a common prefix, like com.acme.a and com.acme.b?
    Yes. The restriction is on identical full package names, not prefixes. com.acme.a and com.acme.b are distinct packages and may live in different modules without conflict.

Two houses claiming the same street address: the postal service (resolver) can't deliver mail because it cannot tell which house owns the address.

saying these in an interview costs you the question

  • Thinking a split package is a runtime ClassNotFoundException deep in code — it actually fails resolution at startup/compile
  • Believing two modules can share a package if versions differ
  • Assuming --add-exports fixes a split (it controls visibility, not ownership)
  • Confusing 'split package' with a circular dependency

context