skip to content

How would you architect a CI matrix that builds and certifies one library against multiple JDK versions using toolchains?

level: seniorimportance: should knowfreq 25%

answer

  1. Maven on one stable JDK, targets installed on image
  2. toolchains.xml on image, consistent vendor strings
  3. parameterize: ${target.jdk} property or per-JDK profiles
  4. guardrail test asserts runtime java.version
  5. release built on lowest supported JDK

basics

~20 s

Run Maven on one modern JDK, install all target JDKs on the agent, generate a toolchains.xml listing them, and run the build once per target by passing the desired version via a property or a profile that sets the toolchain requirement.

solid answer

~50 s

I run Maven itself on a single fast LTS JDK for stability, and provision every target JDK on the CI image. I generate (or commit to the image, not the repo) a toolchains.xml that lists all of them with consistent vendor strings. The build's toolchain requirement is parameterized — typically a property like `<version>${target.jdk}</version>` set per matrix cell, or per-JDK Maven profiles each declaring a different `<jdk>` requirement. The matrix iterates target JDKs (e.g. 11, 17, 21), each running `mvn -t ci/toolchains.xml -Dtarget.jdk=17 verify`. Because the toolchain selects the real JDK for compile, Surefire, and Failsafe, each cell genuinely compiles and runs the test suite on that JDK, certifying the artifact. I add a guardrail test asserting the runtime java.version matches the expected target so a misconfiguration fails loudly instead of silently building on the wrong JDK. The release build picks the lowest supported JDK as the actual baseline.

code

bash · 4 lines
bash
for v in 11 17 21; do
  echo "=== Building on JDK $v ==="
  mvn -t ci/toolchains.xml -Dtarget.jdk=$v clean verify || exit 1
done

go deeper

for a junior

Understands the matrix idea of building on several JDKs.

for a middle

Can install JDKs, write toolchains.xml, and loop the build per version.

for a senior

Designs the parameterization, guardrail tests, and chooses the correct release baseline JDK.

for a principal

Owns the org-wide compatibility matrix policy, golden CI images, provisioning, and the Maven-on-one-JDK vs JAVA_HOME-swap trade-off.

## Goal Produce **one** library artifact (or one per profile) that is *proven* to compile and pass tests on each JDK you support (say 11, 17, 21), while keeping CI fast and reproducible. ## Building blocks 1. **Maven runs on one JDK.** Pick a stable LTS for the Maven process itself; it never changes across the matrix. This avoids re-bootstrapping Maven per cell and isolates 'does Maven run' from 'does the library build on JDK N'. 2. **All target JDKs installed on the agent.** Bake them into the CI image (e.g. `/opt/jdks/temurin-11`, `-17`, `-21`). 3. **A provisioned toolchains.xml.** Lives on the image (config management) or in a CI-only path — not in the app repo (paths are machine-specific). Use **consistent vendor strings** so requirements match reliably. ```xml <toolchains> <toolchain><type>jdk</type> <provides><version>11</version><vendor>temurin</vendor></provides> <configuration><jdkHome>/opt/jdks/temurin-11</jdkHome></configuration></toolchain> <toolchain><type>jdk</type> <provides><version>17</version><vendor>temurin</vendor></provides> <configuration><jdkHome>/opt/jdks/temurin-17</jdkHome></configuration></toolchain> <toolchain><type>jdk</type> <provides><version>21</version><vendor>temurin</vendor></provides> <configuration><jdkHome>/opt/jdks/temurin-21</jdkHome></configuration></toolchain> </toolchains> ``` ## Parameterizing the requirement Two clean approaches: - **Property substitution** — the POM uses `<version>${target.jdk}</version>` in the toolchain requirement; the matrix passes `-Dtarget.jdk=17`. - **Per-JDK profiles** — `<profile id="jdk17">` each sets a different `<jdk>` requirement; the matrix activates one with `-P jdk17`. ## Matrix invocation ```bash for v in 11 17 21; do mvn -t ci/toolchains.xml -Dtarget.jdk=$v clean verify done ``` Each cell compiles, runs Surefire unit tests, and Failsafe integration tests on the *real* JDK $v. ## Guardrail against silent misbuilds The biggest risk is the toolchain silently failing to switch (e.g. typo in vendor → no match → build fails, which is good; or a non-toolchain-aware step running on Maven's JVM). Add a test that asserts the runtime: ```java assertTrue(System.getProperty("java.version").startsWith(expectedMajor)); ``` Wired to read the expected version from a system property, this fails the cell if the wrong JDK ran the tests. ## Release strategy The published artifact should be built/tested on the **lowest** supported JDK (the true compatibility floor) with `<release>` locking the language level, while the higher-JDK cells act as forward-compatibility certification. This way you never ship bytecode or APIs newer than your baseline. ## Why toolchains over just swapping JAVA_HOME per cell You *could* set JAVA_HOME per cell, but then Maven itself re-runs on each JDK (slower, and you risk plugin incompatibilities on bleeding-edge JDKs). Toolchains keep Maven stable and only swap the *build* JDK, which is the cleaner separation.

  • Why keep Maven on one JDK instead of just changing JAVA_HOME per matrix cell?
    Keeping Maven on one stable JDK avoids re-bootstrapping it per cell and plugin incompatibilities on cutting-edge JDKs; toolchains swap only the build JDK, a cleaner separation.
  • Which JDK should the released artifact actually be built on?
    The lowest supported JDK (the compatibility floor), with <release> locking the language level; higher-JDK cells certify forward compatibility but should not raise the baseline.
  • How do you prevent a silent wrong-JDK build in the matrix?
    Add a guardrail test that reads the expected major version from a property and asserts System.getProperty("java.version") matches, failing the cell otherwise.

saying these in an interview costs you the question

  • Committing the machine-specific toolchains.xml (with absolute jdkHome paths) into the application repo.
  • Building the release artifact on the newest JDK in the matrix — that risks shipping bytecode/APIs above the supported floor.

context