Chapter 02Lesson 03~85 minutes

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.

Design TradeoffsToolchainsCompatibilityCIReproducibility

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.
Version baseline — verified 2026-08-23. Labs use JDK 21 as the common build-runtime JDK, Apache Maven 3.9.16, Maven Compiler Plugin 3.15.0, Maven Toolchains Plugin 3.2.0 where toolchain discovery is demonstrated, Gradle 9.7.1, and JUnit Jupiter 5.13.4. Production bytecode is deliberately targeted to Java 17 with the compiler --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.

Production pattern: configure the build first, then configure the IDE to import/delegate to it. Treat IDE-only generated state as local convenience unless it is intentionally versioned and portable.

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/sourceCompatibility alone when strict API compatibility is required and --release is 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?

Why might CI run on JDK 21 while production runs on Java 17?

What is the security concern with automatic toolchain provisioning?

Why is a plain JAR not automatically an executable application distribution?

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.

Next lesson

Diagnose failures from the boundary that broke

Lesson 4 intentionally breaks source-set separation, runtime classpaths, Java targets, resource packaging, and toolchain assumptions, then repairs each failure using the evidence-first diagnostic sequence.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.