Handling Split Packages and Avoiding Conflicts in Java Modules: Best Practices and Real Examples

Illustration for Handling Split Packages and Avoiding Conflicts in Java Modules: Best Practices and Real Examples
By Last updated:

A common mistake developers encounter when migrating to Java Modules is the split package problem. On the classpath, having the same package spread across multiple JARs usually worked fine—though it often introduced hidden risks. But on the module path, the Java Platform Module System (JPMS) strictly prohibits split packages, leading to compilation or runtime errors like:

Error: Packages com.example.utils in module A and module B conflict

This matters because in large enterprise systems or legacy migrations, it’s common for utilities or shared packages to be scattered across different libraries. Without resolving these conflicts, modularization fails, breaking builds and deployment pipelines.

In this tutorial, we’ll explore what split packages are, why they are disallowed, and how to resolve them effectively.


What Are Split Packages?

A split package occurs when the same package name exists across multiple modules.

Example (Classpath World – Works)

app.jar → com.example.utils.StringUtils
lib.jar → com.example.utils.DateUtils

On the classpath, both StringUtils and DateUtils are visible to all code.

Example (Module Path – Fails)

moduleA → com.example.utils.StringUtils
moduleB → com.example.utils.DateUtils

JPMS error:

Packages com.example.utils in moduleA and moduleB conflict

Why JPMS Disallows Split Packages

  • Strong Encapsulation → Prevents ambiguity about which class is being loaded.
  • Predictability → Eliminates runtime surprises from duplicate packages.
  • Security → Reduces the risk of malicious JARs injecting classes into trusted packages.

Real-World Example

Imagine a banking system:

  • user-module contains com.example.common.UserValidator.
  • transaction-module also contains com.example.common.TransactionLogger.

Both declare com.example.common, creating a split package conflict.

Resolution Options

  1. Refactor Packages
// user module
module com.example.user {
    exports com.example.user.validator;
}

// transaction module
module com.example.transaction {
    exports com.example.transaction.logger;
}

Move UserValidator into com.example.user.validator and TransactionLogger into com.example.transaction.logger.

  1. Merge Modules

If the code is tightly coupled, merge into a single common module.

module com.example.common {
    exports com.example.common.validator;
    exports com.example.common.logger;
}
  1. Use Services

Define service interfaces in one module and implement them in others using provides and uses.


Pitfalls & Misuse Cases

  1. Leaving Split Packages Unresolved → Blocks compilation on module path.
  2. Over-Merging Modules → Leads to large “god modules” that defeat modularization.
  3. Package Refactoring Without Care → Breaks backward compatibility for APIs.
  4. Mixing Classpath and Module Path → Can reintroduce split package conflicts.

Best Practices

  • Keep package names unique per module.
  • Create a dedicated common module for shared code.
  • Use service loaders instead of duplicating utilities.
  • Plan refactoring in advance to maintain API stability.
  • Regularly scan dependencies for duplicate packages.

📌 What's New in Java Versions?

  • Java 5 → N/A (modules introduced in Java 9)
  • Java 9 → JPMS introduced with strict prohibition of split packages
  • Java 11 → Tooling improvements to detect split package issues earlier
  • Java 17 → Minor refinements and better diagnostic messages
  • Java 21 → No significant updates across Java versions for this feature

Analogy

Think of modules as office departments:

  • If both HR and Finance maintain files in the same shared cabinet (package), confusion arises over who owns which documents.
  • JPMS solves this by ensuring only one department controls a cabinet. If they both need access, either merge into one shared department or create distinct cabinets.

Summary + Key Takeaways

  • Split packages occur when multiple modules declare the same package.
  • JPMS prohibits split packages to enforce encapsulation and prevent ambiguity.
  • Solutions include refactoring packages, merging modules, or using services.
  • Avoid over-merging or careless refactoring that breaks APIs.
  • Clear boundaries and unique package names keep modular systems maintainable.

FAQs

Q1. What is the difference between the classpath and module path?
Classpath allows split packages and global visibility; module path enforces strict encapsulation.

Q2. Why do I get split package errors when migrating to modules?
Because JPMS does not allow the same package across multiple modules.

Q3. What is the purpose of requires transitive?
It propagates dependencies so downstream modules inherit them without declaring explicitly.

Q4. How do open and opens differ in reflection?

  • open module opens all packages.
  • opens opens specific packages for reflection.

Q5. What are automatic modules, and should I use them?
They’re non-modular JARs treated as modules. Useful during migration but fragile in production.

Q6. How does JPMS improve security compared to classpath?
By disallowing split packages and blocking hidden reflective access.

Q7. When should I use jlink vs jmod?

  • jlink creates custom runtime images.
  • jmod packages modules for distribution.

Q8. Can I migrate a legacy project to modules incrementally?
Yes, but refactor split packages early to avoid conflicts.

Q9. How do I handle third-party libraries that use split packages?
Upgrade to modularized versions or repackage them into a single module.

Q10. Do frameworks like Spring or Hibernate fully support modules?
Partial support—many rely on opens for reflection and may bypass some JPMS restrictions.