Java and JVM Project Structure, Source Sets, Compilation, Testing, Packaging, and Toolchains: Configuration, Design Choices, and Tradeoffs
Choose source layouts, packaging models, compiler targets, build-runtime JDKs, and toolchain strategies deliberately, with explicit tradeoffs for portability, reproducibility, CI, security, and maintenance.
Learning objectives
- Evaluate conventional versus custom source layouts by the configuration and coordination cost they create.
- Choose library versus application packaging based on the consumer/runtime contract rather than the file extension alone.
- Separate the JDK running the build from the JDK/toolchain used by project tasks and the compiler release target.
- Choose between installed, auto-detected, and provisioned toolchains with explicit CI/security boundaries.
- Use a decision table to justify a production JVM build baseline with observable evidence.
--release mechanism so the lesson can separate
“JDK that runs the build” from “Java release the artifact targets.”
Maven 4 preview behavior is not required in this chapter. Re-check
current versions before reusing the examples in production.
1. A build design is a set of contracts
The best build layout is not the one with the most knobs. It is the one that makes important contracts obvious: where production/test code lives, what Java release consumers can rely on, which JDKs are allowed to compile/test, what artifact is produced, and how CI reproduces those choices. Every deviation from convention is a maintenance obligation.
2. Conventional directories versus custom source layout
Convention gives Maven, Gradle, IDEs, code search, and new contributors a shared vocabulary. Custom layouts are sometimes required for legacy migration, generated sources, mixed-language repositories, or integration-test separation, but they should solve a concrete problem.
| Choice | Advantages | Costs / risks | Evidence to require |
|---|---|---|---|
conventional src/main/src/test
|
minimal configuration; familiar tooling | less freedom for legacy structures | output tree and IDE/CLI agreement |
| custom production directory | supports migration/legacy layout | extra build + IDE config; higher onboarding cost | effective source directories from build model |
| new source set/test suite | stronger lifecycle/classpath separation | more configurations/tasks/reports to govern | task/source-set listing and test accounting |
| generated source directory | supports code generation | must declare generator inputs/outputs/order | generated path, producer task/plugin, reproducibility evidence |
If a custom directory is chosen, document who produces it, whether it is authored or generated, which compile task consumes it, and whether it enters the published artifact.
3. Library versus application packaging
A library is primarily consumed by another build through coordinates and metadata. An application is primarily executed and may need a main class, runtime dependency distribution, launcher script, container image, or platform-specific package. A plain JAR can serve either role only if the surrounding runtime contract is clear.
| Question | Library-oriented answer | Application-oriented answer |
|---|---|---|
| Who is the consumer? | another compiler/build | a JVM/process/operator |
| Key compatibility concern | public API/ABI and dependency metadata | runtime JDK, configuration/resources, dependency delivery |
| Typical evidence | published module metadata + API tests | startup/run test + package/distribution contents |
| Do dependencies live inside the JAR? | usually no | not necessarily; depends on packaging strategy |
4. Build-runtime JDK versus compiler release target
Running the build on a newer JDK can unlock a supported Maven/Gradle
runtime while still producing code for an older deployment baseline.
That separation is useful, but only if encoded deliberately.
Depending only on JAVA_HOME makes the compiler identity
ambient and allows developer/CI drift.
<properties>
<maven.compiler.release>17</maven.compiler.release>
</properties>
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
tasks.withType<JavaCompile>().configureEach {
options.release = 17
}
The Gradle example says two things: project JVM tasks should use Java 21, and Java compilation should emit/validate against Java 17. Maven can express the release target in the POM and select a different JDK through Maven Toolchains where required.
5. Installed, auto-detected, or provisioned toolchains
Toolchain auto-detection reduces manual path configuration, but “found a JDK” is not a provenance policy. CI images often deliberately preinstall approved JDKs. Auto-provisioning can improve developer ergonomics, but it introduces a download repository and distribution trust decision that should be controlled by organization policy.
| Strategy | Best fit | Tradeoff | Production control |
|---|---|---|---|
| preinstalled approved JDKs | locked-down CI, regulated environments | image maintenance/upgrades | image digest + JDK vendor/version inventory |
| local auto-detection | developer workstations | machine variability | document accepted vendors/versions; inspect selection |
| toolchain provisioning | onboarding, ephemeral agents | network/provenance dependency | approved resolver/repository + checksum/vendor policy |
| hard-coded absolute JDK path | rare local experiments | non-portable and leaks machine layout | avoid in committed project config |
6. IDE settings are a developer convenience, not the authoritative CI contract
An IDE can have a project SDK, language level, compiler settings,
and its own build delegation behavior. Those settings may be useful,
but the repository build must remain authoritative. If “Build in
IDE” passes while ./mvnw verify or
./gradlew build fails, diagnose the model/toolchain
difference rather than weakening the CLI build.
7. Toolchain convenience has a supply-chain boundary
JDK archives, wrapper distributions, plugins, annotation processors, and dependencies all execute or influence code during the build. An auto-downloaded JDK is therefore not merely “developer tooling”; it is executable input. Prefer trusted vendor/distribution sources, verify wrapper and distribution integrity according to current tooling, and do not give untrusted builds broad credentials.
8. Worked decision — internal Java service with Java 17 production runtime
Assume an organization’s runtime platform is Java 17, developer workstations commonly have JDK 21, and CI uses an approved JDK 21 image. The service has a normal production/test split and publishes an application JAR.
| Decision | Selected approach | Why | Verification |
|---|---|---|---|
| source layout | conventional main/test directories | lowest configuration/onboarding cost | source/output tree |
| build runtime | JDK 21 | supported common Maven/Gradle baseline | wrapper version output |
| compiler API/bytecode target | --release 17 |
matches deployment runtime contract | javap -verbose + runtime smoke test |
| toolchain | project-declared/approved JDK 21 | avoids ambient PATH drift |
toolchain report |
| tests | JUnit on test-only classpath | keeps framework out of production contract | test count + dependency model |
| artifact | plain JAR + explicit runtime dependency strategy | clear package identity | JAR listing + checksum |
This design is not universally correct; it is correct because each choice matches a stated constraint and has observable evidence.
9. Design anti-patterns
- Changing source directories solely to make the repository “look cleaner.”
-
Using
targetCompatibility/sourceCompatibilityalone when strict API compatibility is required and--releaseis available. - Assuming a toolchain’s Java version and the deployment target must always be identical.
- Hard-coding local JDK paths or relying on IDE-only compiler settings.
- Letting toolchain provisioning download from an unreviewed third-party source.
- Packaging test fixtures or test framework classes into the production artifact accidentally.
Knowledge check
When is a custom source layout justified?
When it solves a concrete constraint such as legacy migration, generated sources, or a deliberate additional source set—and the extra configuration/IDE/CI cost is documented and tested.
Why might CI run on JDK 21 while production runs on Java 17?
The build tool can require/use a newer JVM while the compiler release target and runtime tests enforce Java 17 compatibility.
What is the security concern with automatic toolchain provisioning?
It downloads executable build input. The resolver/repository, distribution identity, integrity, and allowed vendor/version policy become supply-chain controls.
Why is a plain JAR not automatically an executable application distribution?
A plain JAR may lack an entry point and usually does not bundle all runtime dependencies; the surrounding runtime/dependency contract must be explicit.
Summary
Source layout, packaging, compiler target, build-runtime JDK, and toolchain selection are independent design choices. Prefer conventions until a real requirement justifies deviation, encode compatibility in the repository build, keep IDE state subordinate to CLI/CI, and treat provisioned JDKs as supply-chain inputs.
Official references and version notes
These lessons were finalized against current primary documentation on 2026-08-23. Build-tool, plugin, JDK, repository, and IDE behavior is version-sensitive; verify the exact versions used by your project and CI before applying a production policy.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.