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
}| Modifier | Same class | Same package | Subclass | Anywhere |
|---|---|---|---|---|
private | Yes | No | No | No |
| (default) | Yes | Yes | No | No |
protected | Yes | Yes | Yes | No |
public | Yes | Yes | Yes | Yes |
- 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.javamust sit at the root of the source set, one per module, and it is compiled before everything else.exportsworks 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 transitivere-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-Classmanifest 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.
Related
Files, I/O and JSON Setting up a modern Java toolchain
Last refreshed 2026-09-18.