Java Development

Mastering Java Modules: My 2025 Same-Package Strategy

Unlock advanced Java module patterns for 2025. Learn the 'Same-Package Strategy' to solve complex testing and framework design challenges in JPMS.

D

Daniel Ivanov

Senior Java Architect specializing in modular systems and high-performance application design.

7 min read4 views

Introduction: Moving Beyond JPMS Basics

Since its introduction in Java 9, the Java Platform Module System (JPMS), or Project Jigsaw, has been a cornerstone of modern Java development. It promised an end to the infamous "classpath hell" by providing strong encapsulation, reliable configuration, and improved security. For years, the community has adhered to its core tenets, one of the most rigid being: a package must belong to exactly one module.

But as our modular applications grow in complexity, we encounter edge cases where this strict rule can feel more like a hindrance than a help, especially in testing and advanced framework design. This is where the Same-Package Strategy comes in. In 2025, this isn't about recklessly breaking the rules; it's about understanding them so well that you know when and how to bend them. This article dives deep into this advanced strategy, showing you how to leverage same-package semantics across different source sets to enhance testability and internal code cohesion without sacrificing the benefits of JPMS.

The Split-Package Dilemma: Why JPMS Says No

Before we can master the strategy, we must first respect the problem it navigates: the split package. Understanding why JPMS forbids it is crucial to applying this technique responsibly.

What Exactly Is a Split Package?

A split package occurs when two or more modules on the module path contain classes in the same package. For example, if both `module-a` and `module-b` contain classes in the `com.example.util` package, you have a split package. When the Java runtime tries to load a class from `com.example.util`, it faces an ambiguity: which module's version of the package should it use? This leads to unpredictable behavior and crashes.

The Rationale Behind the Restriction

JPMS enforces the "one package, one module" rule to guarantee reliability and prevent the following issues:

  • Ambiguity: The runtime doesn't have to guess where a package's resources are located.
  • Version Conflicts: It prevents a situation where two different versions of a library (each in its own module) try to own the same package, leading to chaos.
  • Unreliable Encapsulation: If another module could inject classes into your package, your `package-private` access modifier would become meaningless.

The prohibition of split packages is a foundational feature, not a bug. Our strategy, therefore, isn't about creating true split packages at runtime but about using a similar structure at build time for specific, controlled purposes.

The 2025 Same-Package Strategy: A Controlled Approach

The Same-Package Strategy is the practice of placing code in the same package name across different source sets (e.g., `src/main/java` and `src/test/java`) and using build tools to "patch" them together for a specific phase, like compilation or testing.

The Core Concept: It's Not Your Grandfather's Split Package

The key distinction is that these "split" parts of the package are never exported and exposed to the consumer application simultaneously. Instead, one part (the test code) is virtually merged into the main module during the test phase. For the final application artifact (the JAR), only the code from `src/main/java` exists in the module. The runtime environment of your production application never sees a split package.

Primary Use Case: Seamless White-Box Testing

This is the most common and compelling reason to use the strategy. Imagine you have a class with several `package-private` methods and fields that contain complex logic. You want to test this logic directly without making the methods `public` and polluting your module's public API.

By placing your test class in the same package within your `src/test/java` directory, you can naturally access these package-private members. The build tool will then use a mechanism to make the test code see the main code as if they were compiled together.

Example:

  • Main Code: `src/main/java/com/myapp/service/OrderProcessor.java` (has package-private methods)
  • Test Code: `src/test/java/com/myapp/service/OrderProcessorTest.java` (can directly call the package-private methods of `OrderProcessor`)

Advanced Use Case: Cohesive Internal Frameworks

For large, internal frameworks, you might have a core module and several extension modules (e.g., for persistence, messaging, etc.). Sometimes, an extension needs deep, privileged access to the core module's internals. Instead of making those internals public, you can place specific integration classes in the same internal package within the extension module. You would then use qualified `opens` or other advanced `module-info` directives to grant access strictly between these two framework modules, keeping the implementation details hidden from the end-user.

Warning: This approach is for experts and requires a deep understanding of the module system and a disciplined team. It can create tight coupling if not managed carefully.

Implementation in Practice: Tools and Techniques

This strategy relies on specific compiler and build tool features that have matured since Java 9.

The Magic of `--patch-module`

The secret sauce is the `javac` and `java` command-line option: `--patch-module`. This option tells the JVM to treat the code in a given directory as if it were part of the specified module. When your build tool runs tests, it effectively does this:

--patch-module com.myapp=src/test/java

This command tells the module system: "For the `com.myapp` module, I want you to overlay the contents of the `src/test/java` directory." This makes your test classes first-class citizens of the module during the test run, granting them access to package-private members.

Build Tool Integration: A Maven Example

You don't need to manage this manually. Modern build tools handle it for you. In Maven, the `maven-surefire-plugin` (for running tests) automatically detects that `src/main/java` and `src/test/java` should be part of the same logical module for testing.

You simply need to maintain a standard project structure and your `module-info.java` file in `src/main/java`. You do not put a `module-info.java` in your test source directory. The plugin takes care of the patching.

If you need more explicit control, you can configure it directly:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.2.5</version>
  <configuration>
    <argLine>
      --patch-module com.myapp=${project.build.testOutputDirectory}
    </argLine>
  </configuration>
</plugin>

Gradle has similar mechanisms, often requiring even less configuration.

Strategy Comparison: Same-Package vs. Traditional Methods

Comparing Module Organization Strategies
FeatureSame-Package StrategyTraditional (Separate Test Utility Package)Traditional (Expose via Public API)
TestabilityExcellent (access to package-private members)Limited (only public members)Good (but pollutes the public API)
API CleanlinessExcellent (no internal methods are exposed)Excellent (API remains clean)Poor (API bloated with test-only methods)
EncapsulationStrong (if managed correctly)Very StrongWeakened (implementation details leak)
Code OrganizationCo-located and logical (tests next to code)Separated (test helpers in another module)N/A
Build ComplexityLow to Moderate (relies on build tool magic)LowVery Low
Risk of ErrorModerate (requires discipline to avoid leaks)Very LowLow

Conclusion: A Powerful Tool for the Expert's Kit

The Same-Package Strategy is a testament to the maturity of the Java Module System and its surrounding tooling. It's an acknowledgment that sometimes, for pragmatic reasons like effective testing, we need controlled access to a module's internals. By leveraging `--patch-module`, we can achieve the best of both worlds: clean, minimal public APIs and thorough, direct white-box tests.

This is not a free-for-all invitation to create split packages. It's an advanced, precise technique for a specific set of problems. As we move through 2025, mastering such patterns will distinguish proficient Java developers from true module system artisans. Use it wisely, and it will become an invaluable tool in your software architecture toolbox.