Executive Summary
I just read a piece from two people at Info Support (Maarten Mulders and Kasper van de Wetering) sharing their real-world experience setting up the Maven Build Cache Extension in their Azure DevOps pipeline. What's interesting isn't "how to enable the cache"—that's the easy part—but the list of small issues that only surfaced after the cache was running, and how they resolved them one by one.
Problems Solved
This build cache originated as a donation from the Deutsche Bank team to Apache Maven in 2019, which they say cut build times from 45-60 minutes down to 1-2 minutes in their own environment. The idea is similar to the Gradle build cache: instead of relying on file timestamps (not reliable across platforms/filesystems), the extension uses a fast non-cryptographic hash like XX-hash to detect whether a module's inputs have changed. Because the cache works per Maven module—not for the whole project—the more small modules a project has, the greater the potential savings: a module untouched in a given PR can be restored straight from cache without recompiling/retesting/repackaging. This is a good fit for PR validation pipelines that often trigger a full build even though only one or two modules changed.
Key Features and Code Examples
The basic setup just adds extensions.xml in the .mvn folder:
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0">
<extension>
<groupId>org.apache.maven.extensions</groupId>
<artifactId>maven-build-cache-extension</artifactId>
<version>1.3.0</version>
</extension>
</extensions>But because the Azure DevOps build agent always starts from a clean workspace, the local cache at ~/.m2/build-cache is lost every run. They chose the simpler option compared to a remote repository, namely the Cache@2 task in the pipeline:
- task: Cache@2
inputs:
key: 'maven-build-cache | "$(Agent.OS)" | pom.xml'
path: $(MAVEN_BUILD_CACHE_FOLDER)
restoreKeys: |
maven-build-cache | "$(Agent.OS)"The point I noted: they deliberately avoided making the cache key too specific (e.g., including the entire contents of pom.xml), because the extension already computes its own fingerprint per module—if the Azure DevOps key were too detailed, it would just duplicate Maven's own logic and actually increase cache misses.
Three concrete issues they ran into are quite instructive:
- Restoring the artifact alone isn't enough. By default the cache only restores the final artifact (jar/war/ear), but tools like JaCoCo and Surefire need the
target/classes,target/test-classes,target/surefire-reports,target/site/jacocofolders. The fix was addingattachedOutputsinmaven-build-cache-config.xmlso those directories get saved too.
- SonarCloud conflicts with the reactor. The
sonar:sonargoal stands on its own, not a lifecycle phase, so the Build Cache Extension tries to restore it as if it were a regular phase—and fails on the second build. The fix was splitting it into twoMaven@4tasks: one forpackage, one forsonar:sonar. But this created a new problem: the second Maven process loses the reactor context, so internal cross-module dependencies couldn't be found in the local repo (producing a "could not be resolved... but seem to be part of the reactor" error). They ended up changing the first goal frompackagetoinstall, and forcing theinstallgoal to always run viaexecutionControl/runAlways(targetingmaven-install-plugin) in the config XML, even when the module is restored from cache.
- OWASP Dependency Check was also split out. Because this plugin is tied to the dependency graph and vulnerability data that keeps changing—not a reusable build output—they pulled it out of the main execution and ran it as a separate
org.owasp:dependency-check-maven:aggregategoal in its own Maven task.
The deployment pipeline deliberately doesn't use the cache at all—they want the deployed artifact to always be the result of a clean build.
Impact and Upgrade Notes
The numbers they reported: before the cache, a full PR build took around 18 minutes (14 minutes for compile/test/package, with the remaining 4 minutes for pre/post-steps plus SonarCloud analysis). The first build on a branch is still a full build as usual (nothing to restore yet), but subsequent builds immediately showed the difference—the Maven install step could be as fast as 1 minute in the best case, averaging 5-6 minutes, bringing the total pipeline duration down by around 50%.
Something worth being aware of before adopting this: the Azure DevOps cache is immutable and scoped per branch, so every new PR branch still needs one full build before it can benefit from the cache (they considered "priming" the cache from the main branch so even the first PR would be faster, but this hasn't been implemented yet). No minimum Maven version is mentioned beyond extension 1.3.0 and the Azure DevOps-specific Maven@4/Cache@2 tasks—if your CI stack is different (GitLab, GitHub Actions), the principle is the same but you'll need to figure out the cache-persistence-between-runs mechanism yourself. Their practical advice that I think is most useful: measure your baseline before changing any configuration, check exactly what files the steps after the build actually need (don't assume the final artifact alone is enough), and be careful when splitting build & analysis into separate Maven invocations—the reactor context gets lost along with it.
---
First announced on Foojay.io (Friends of OpenJDK). Read the original source for full details.