Cache Java Dependencies in CI Pipelines

Khimananda Oli 8 min read Programming and Languages
Cache Java Dependencies in CI Pipelines

By Khimananda Oli | Last reviewed: August 2026

Slow Java builds are rarely caused by compilation; they are almost always caused by downloading the same 500MB of JARs from Maven Central or Artifactory on every single run. When you fail to cache Java dependencies in CI pipelines, you waste compute minutes, burn through egress bandwidth budgets, and create a fragile dependency on external repository availability. This guide provides the exact configuration patterns for Maven and Gradle across major CI platforms to eliminate redundant network I/O.

Source Codepom.xml / build.gradleHash GeneratorSHA-256(dep-file)Key: deps-abc123CI Cache Store~/.m2/repositoryOR ~/.gradle/cachesHIT → RestoreMISS → DownloadBuild & Test Phase
Dependency caching flow: source hash determines cache key for restoring Java artifacts in CI pipelines

How do you configure dependency caching in GitHub Actions for Maven?

GitHub Actions remains the most common platform for Java projects in 2026, yet many teams still use naive caching strategies that result in frequent cache misses. The official actions/cache action is powerful but requires precise path and key configuration to work reliably with Maven. A common mistake is caching the entire home directory or using a static key that never invalidates, leading to stale artifacts. For broader pipeline context, see our guide on build caching speed up CI builds.

Optimal Maven cache configuration

Maven stores dependencies in ~/.m2/repository. You must hash your pom.xml files to generate a unique key. If you have a multi-module project, hashing only the root POM is insufficient because child modules may declare independent dependencies.

- name: Cache Maven dependencies
  uses: actions/cache@v4
  with:
    # Cache the local repository where JARs are stored
    path: ~/.m2/repository
    # Primary key: OS + hash of ALL pom.xml files
    key: ${{ runner.os }}-maven-${{ hashFiles('/pom.xml') }}
    # Fallback: restore partial cache if exact match fails
    restore-keys: |
      ${{ runner.os }}-maven-
  • Path precision: Never cache ~/.m2 entirely. The settings.xml and wrapper directories should remain uncached to avoid masking configuration changes.
  • Hash globbing: The /pom.xml pattern ensures that any dependency change in any submodule triggers a new cache entry.
  • Restore keys: The fallback ${{ runner.os }}-maven- allows GitHub to restore the most recent cache even if the hash doesn't match exactly. This means you only download changed dependencies rather than everything from scratch.

Handling Maven wrapper and settings

If your team uses custom settings.xml for internal Nexus or Artifactory mirrors, include it in the hash. Otherwise, a cache created with public Maven Central will be restored for builds expecting private artifacts, causing cryptic resolution failures.

key: ${{ runner.os }}-maven-${{ hashFiles('/pom.xml', '.mvn/settings.xml') }}

What is the correct way to cache Gradle dependencies in CI?

Gradle's caching architecture is fundamentally different from Maven's. It uses a content-addressable store in ~/.gradle/caches/modules-2/files-2.1 plus metadata in modules-2/metadata-*. Caching the entire ~/.gradle directory is a known anti-pattern that causes build failures due to corrupted lock files and daemon state.

Targeted Gradle cache paths

In practice, you need to cache three specific directories to get full benefit without side effects:

- name: Cache Gradle packages
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches/modules-2/files-2.1
      ~/.gradle/caches/modules-2/metadata-*
      ~/.gradle/wrapper/dists
    key: ${{ runner.os }}-gradle-${{ hashFiles('/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

The gradle-wrapper.properties inclusion is critical. If you upgrade the Gradle version but don't invalidate the cache, the old wrapper distribution may conflict with the new metadata format. For teams managing complex Java automation, understanding Gradle build automation fundamentals helps prevent these subtle issues.

Why not use the setup-gradle action?

As of 2026, the gradle/actions/setup-gradle@v4 action includes built-in caching that handles these paths automatically. However, explicit caching gives you auditability and control over retention policies. In regulated environments where I've implemented SOC 2 controls, auditors prefer visible cache configurations over implicit magic. Use the built-in action for greenfield projects; use explicit caching when compliance or debugging requires transparency.

Maven Structure~/.m2/repository/org/springframework/spring-core/6.2/spring-core-6.2.1.jarspring-core-6.2.1.pomFlat hierarchyEasy to cache entirelyStale JARs accumulateGradle Structure~/.gradle/caches/modules-2/files-2.1/org.springframework/metadata-2.106/descriptors/transforms-3/ (content-addressable)Content-addressable storeDeduplication built-inSelective caching required
Maven vs Gradle cache layout differences affecting how you cache Java dependencies in CI pipelines

How does dependency caching differ across GitLab CI, Jenkins, and Azure DevOps?

While GitHub Actions dominates open source, enterprise Java shops frequently use GitLab CI, Jenkins, or Azure Pipelines. Each has distinct caching primitives that affect how you implement dependency persistence.

PlatformCache MechanismKey StrategyRetention PolicyBest For
GitHub Actionsactions/cache@v4Explicit hash + restore-keys7 days unused, 10GB maxOSS, mid-size teams
GitLab CIcache:key:filesFile-based auto-hashPer-branch, configurableMonorepos, self-hosted
JenkinsJob Cacher plugin / S3Manual key managementIndefinite (external storage)On-prem, air-gapped
Azure PipelinesCache@2 taskExplicit key + runtime OS7 days unused, 10GB max.NET/Java hybrid shops

GitLab CI file-based keys

GitLab's native cache syntax is more declarative. The key:files directive automatically hashes specified files, eliminating manual hashFiles() calls:

cache:
  key:
    files:
      - pom.xml
      - .mvn/settings.xml
  paths:
    - .m2/repository/
  policy: pull-push

Note the policy: pull-push setting. For feature branches that run frequently but rarely change dependencies, set policy: pull to avoid wasting time uploading unchanged caches. Only merge-to-main jobs should push updates.

Jenkins and persistent storage

Jenkins lacks built-in ephemeral caching. In production environments I've managed, we used the Job Cacher plugin backed by S3-compatible storage. For teams operating under strict data residency requirements in Nepal or similar jurisdictions, this can point to local MinIO instead of cloud storage. See our self-hosted CI runners setup guide for secure agent configurations that support this pattern.

Why are my cached dependencies still causing slow builds?

Even with correct configuration, several subtle issues undermine cache effectiveness. These are the failure modes I encounter most often during infrastructure audits.

Non-deterministic dependency resolution

If your pom.xml uses version ranges like [1.0,2.0) or SNAPSHOT dependencies without pinned timestamps, Maven resolves different artifacts on each run despite an identical POM hash. Always pin exact versions in CI. Use the versions-maven-plugin to lock transitive dependencies before committing.

Cache size limits and eviction

GitHub Actions enforces a 10GB total cache limit per repository. Large Java monorepos easily exceed this. When the limit is hit, GitHub silently evicts the oldest entries, causing repeated misses. Monitor cache usage via the Actions cache API and implement prefix-based partitioning:

key: ${{ runner.os }}-maven-${{ matrix.module }}-${{ hashFiles(format('{0}/pom.xml', matrix.module)) }}

This partitions caches by module, allowing granular eviction rather than losing everything at once.

Timezone and timestamp corruption

Some Maven plugins write timestamps into cached metadata files. If your CI runners span multiple timezones or use inconsistent NTP sources, these timestamps drift and trigger unnecessary re-downloads. Set TZ=UTC explicitly in your pipeline environment and ensure all runners synchronize time before build steps execute.

Cache Miss DetectedDid dep file hash change?YESNOExpected behaviorNew cache will be createdProblem: Check theseEviction / Paths / TZ1. Cache size > 10GB? Evicted?2. Correct paths cached?3. TZ=UTC on all runners?
Troubleshooting decision tree for cache Java dependencies in CI pipelines when misses occur unexpectedly

Should you use a remote artifact repository instead of CI caching?

CI-level caching and remote repositories solve overlapping but distinct problems. Understanding when to use each prevents architectural confusion.

CI caching reduces network latency between the runner and storage. A remote repository like Nexus, Artifactory, or AWS CodeArtifact reduces external dependency risk and provides governance. In mature setups, you use both: the remote repo acts as the authoritative source, while CI caching avoids hitting even that internal endpoint repeatedly.

For teams in Nepal or regions with limited international bandwidth, a local Artifactory instance dramatically improves baseline performance regardless of CI caching. The cache then becomes a second-layer optimization for intra-day builds. Budget-conscious startups should evaluate cloud cost optimization tactics before investing in premium artifact hosting, as egress fees often outweigh subscription costs at moderate scale.

A practical rule: implement CI caching first—it's free and immediate. Add a remote repository when you need vulnerability scanning, license compliance, or offline resilience. Never treat CI cache as your sole artifact store; it is ephemeral by design and can disappear without warning.

Implementing Dependency Caching Today

Start by auditing your current pipeline logs. Search for "Downloading from central" or "Resolved dependencies" timestamps. If you see consistent download phases exceeding 30 seconds, caching will yield immediate returns. Apply the configurations above, verify cache hits in subsequent runs, and monitor your CI provider's cache usage dashboard weekly. Properly configured dependency caching typically reduces Java build times by 40–70% and cuts monthly compute spend proportionally. If your team needs help designing compliant, high-performance CI infrastructure, reach out to discuss your specific requirements.

Frequently Asked Questions

Use the setup-java action with cache parameter set to maven. This automatically caches the .m2 repository between workflow runs, reducing build times significantly without manual configuration or extra steps.

Most CI systems hash gradle.lockfile or build.gradle files to generate keys. This ensures caches invalidate only when dependency declarations change, preventing stale artifact issues during pipeline execution.

Yes, configure restore-keys with branch prefixes to allow fallback matching. This lets feature branches reuse main branch caches while maintaining isolation for dependency updates specific to each branch.

Check that cache paths match your package manager configuration exactly. Verify environment variables like GRADLE_USER_HOME are consistent across save and restore steps to prevent path mismatches.

Absolutely. Cached builds skip redundant downloads, cutting network egress fees and compute minutes by thirty to fifty percent on average for typical microservice projects in 2026.

Set limits between two and five gigabytes depending on project scope. Monitor cache hit rates and evictions weekly to adjust thresholds based on actual dependency churn patterns.

Yes, if you exclude credentials from cached paths and use scoped tokens. Never cache settings.xml containing passwords; instead inject secrets at runtime via environment variables.

Implement checksum validation in post-cache steps. Configure automatic cache invalidation on verification failure to trigger fresh downloads rather than propagating broken artifacts through downstream jobs.

Only if your monorepo uses both build tools. Separate cache keys prevent conflicts, but dual caching increases storage costs unnecessarily for single-tool projects.

Enable verbose logging for cache operations and inspect job logs for key mismatches. Compare generated keys against expected values using echo statements before cache restoration steps.

No, they serve different purposes. Layer caching speeds image builds but does not persist Maven or Gradle repositories across container restarts or separate pipeline stages.

Invalidate weekly or on lockfile changes, whichever comes first. Scheduled invalidation prevents accumulation of unused transitive dependencies that bloat cache storage over time.

Self-hosted runners retain filesystem state between jobs natively. You can skip cloud cache uploads entirely and rely on local disk persistence for faster access and zero transfer costs.

Properly configured caching improves reproducibility by ensuring identical dependency resolution. Always pin versions in lockfiles and avoid dynamic version ranges that create non-deterministic cache states.

Performance varies by infrastructure proximity and compression algorithms. Benchmark your specific workload across platforms using identical cache sizes to determine optimal choice for your team.