How would you architect a CI matrix that builds and certifies one library against multiple JDK versions using toolchains?
answer
- Maven on one stable JDK, targets installed on image
- toolchains.xml on image, consistent vendor strings
- parameterize: ${target.jdk} property or per-JDK profiles
- guardrail test asserts runtime java.version
- release built on lowest supported JDK
basics
~20 sRun 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 sI 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 linesfor v in 11 17 21; do
echo "=== Building on JDK $v ==="
mvn -t ci/toolchains.xml -Dtarget.jdk=$v clean verify || exit 1
donego deeper
Understands the matrix idea of building on several JDKs.
Can install JDKs, write toolchains.xml, and loop the build per version.
Designs the parameterization, guardrail tests, and chooses the correct release baseline JDK.
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.