Modules, packages and build configuration

Packages and visibility, JPMS modules, multi-module Maven and Gradle builds, and producing a runnable jar.

Packages and visibility

package com.example.orders;          // the directory must match exactly

public class OrderService {

    public Receipt place(Order order) { ... }      // the published API
    Receipt recalculate(Order order) { ... }       // package-private helper

    private BigDecimal taxFor(Order order) { ... } // visible only inside this class
}
ModifierSame classSame packageSubclassAnywhere
privateYesNoNoNo
(default)YesYesNoNo
protectedYesYesYesNo
publicYesYesYesYes
  • A package is a naming and visibility boundary, not a security boundary — code in the same package can always reach package-private members.
  • Start from the smallest useful visibility and widen only when a caller needs it; a public method is a promise you have to keep.
  • Package names are conventionally reverse-domain and all lowercase, and the directory tree must match the declarations exactly.
  • Group by feature rather than by technical layer in a growing codebase: the package that changes together stays together.

JPMS modules

// src/main/java/module-info.java
module com.example.orders {
    requires java.sql;
    requires transitive com.example.core;    // callers also see core

    exports com.example.orders.api;          // only this package is public
    opens com.example.orders.model to com.fasterxml.jackson.databind;

    uses com.example.orders.spi.Pricer;
    provides com.example.orders.spi.Pricer
        with com.example.orders.internal.SimplePricer;
}
  • module-info.java must sit at the root of the source set, one per module, and it is compiled before everything else.
  • exports works per package: a package that is not exported is invisible even if every class in it is public.
  • Reflection needs opens, which is why JSON and persistence libraries often require an explicit directive.
  • requires transitive re-exports a dependency so callers do not have to declare it themselves; use it when your API exposes that library's types.
  • A module import declaration (import module com.example.core;) brings a module's exported packages into scope without naming each class, which suits small files and prototypes.
  • Everything runs on the classpath as well; migration can be incremental, one module at a time.
⚠️
Split packages break modular builds: two modules may not contain the same package. That is what stops a jar from adding a class to com.example.orders that already exists in another jar, and it is usually the first thing that fails when a fat jar is converted to modules.

Multi-module builds and runnable jars

<!-- parent pom.xml: the reactor builds these in dependency order -->
<modules>
  <module>core</module>
  <module>api</module>
</modules>
mvn -q clean verify                 # compile, test and package every module
java -jar api/target/api-1.0.0.jar  # only works with a manifest and dependencies inside

jar --describe-module --file api/target/api.jar
java --module-path mods --module com.example.orders/com.example.orders.Main
  • Keep dependencies flowing one way. A cycle between modules means the boundary is wrong, and no build tool can fix that for you.
  • Centralise versions in a parent POM's dependency management or a Gradle platform so every module resolves the same library versions.
  • An executable jar needs a Main-Class manifest entry and either a shaded fat jar or a classpath of neighbouring jars; the shade plugin hides module boundaries and is for services, not libraries.
  • Pin plugin versions. A build that resolves a different compiler or test plugin next month is not reproducible.
  • Prefer a library's module form when it publishes one; automatic modules from plain jars work but lose the explicit export list.

FAQ

Do I need JPMS for a normal Spring application?
No. Most services run happily on the classpath, and frameworks rely heavily on reflection that would need opens directives everywhere. Use modules when you publish a library, ship a bounded tool, or need a small runtime image with jlink.
How many modules should a project have?
As few as the boundaries allow. Each module is a compile-time contract you must maintain, so split when two areas genuinely evolve at different rates — not to mirror the package tree.

Files, I/O and JSON Setting up a modern Java toolchain

Last refreshed 2026-09-18.