skip to content

How would you design dependency-cache handling across a CI fleet for fast, reproducible builds?

level: principalimportance: should knowfreq 25%

answer

  1. persist GRADLE_USER_HOME keyed on lockfiles
  2. fixed versions + dependency locking
  3. warm then --offline
  4. dependency verification for integrity
  5. mirror via Artifactory/Nexus; per-OS keys

basics

~10 s

Persist and restore GRADLE_USER_HOME (the modules-2 cache) as a CI cache keyed on lockfiles, keep release builds on fixed/locked versions, and use --offline once the cache is warm to make builds fast and network-independent.

solid answer

~50 s

The goal is **fast** (don't re-download every run) and **reproducible** (same inputs → same bytes). Strategy: (1) **Persist `GRADLE_USER_HOME`** — specifically `caches/modules-2` — as a CI cache layer, keyed on the dependency lockfiles / catalog so the key changes only when dependencies change. (2) **Eliminate mutability** in releasable builds: only fixed versions, no SNAPSHOTs or dynamic selectors, and enable **dependency locking** to pin transitive versions; that makes the cache deterministic. (3) Once the cache is warm, run jobs **`--offline`** so a flaky repository can't break or slow the build and nothing escapes to the network. (4) For supply-chain integrity, enable **dependency verification** (checksum/signature metadata) so a poisoned cache entry is rejected. (5) Optionally front everything with an **internal mirror/proxy** (Artifactory/Nexus) for availability and audit. Avoid checking the cache into VCS, and use distinct cache keys per OS/arch to prevent cross-platform contamination.

code

kotlin · 7 lines
kotlin
// settings.gradle.kts or build script: pin everything
dependencyLocking { lockAllConfigurations() }

// Generate/refresh locks intentionally:
//   ./gradlew dependencies --write-locks
// CI build step (after cache restore):
//   ./gradlew build --offline

go deeper

for a junior

Not expected; basic awareness that CI caches the ~/.gradle directory.

for a middle

Describe restoring GRADLE_USER_HOME as a CI cache and warming it before offline builds.

for a senior

Combine lockfile-keyed caching, dependency locking, and --offline; explain the reproducibility rationale.

for a principal

Add supply-chain integrity (dependency verification), mirrors/proxies, per-platform keys, and org-wide governance of mutable versions.

## The two objectives A CI dependency strategy balances: - **Speed** — avoid re-downloading the world on every job. - **Reproducibility & integrity** — the same source commit resolves to the same, trustworthy bytes every time, on every runner. ## Building blocks ### 1. Persist the dependency cache Cache **`GRADLE_USER_HOME`** (at minimum `caches/modules-2`) between jobs using the CI's native caching. **Key the cache on the lockfiles** (or `gradle/libs.versions.toml` + `*.lockfile`) so the cache is reused while dependencies are unchanged and rebuilt only when they change. Use restore-keys for partial reuse. Don't cache volatile per-build state. ### 2. Remove mutability The cache is only deterministic if resolution is. In **releasable builds**, ban `-SNAPSHOT` and dynamic versions (`1.+`, `latest.release`), and turn on **dependency locking** so even transitive versions are pinned: ```kotlin dependencyLocking { lockAllConfigurations() } // ./gradlew dependencies --write-locks to (re)generate gradle.lockfile ``` Now a warm cache + lockfile = byte-identical resolution. ### 3. Go offline once warm After the cache-restore step, run the actual build with **`--offline`**. Benefits: immune to repository outages/slowness, and a hard guarantee that the build performs no dependency network I/O. A missing module then fails fast — which is desirable, because it means your cache key or warming step is wrong. ### 4. Verify integrity Enable **dependency verification** (`gradle/verification-metadata.xml` with SHA-256 / PGP). This protects against a poisoned shared cache or a compromised mirror: an entry whose checksum doesn't match the recorded value is rejected. ### 5. Mirror / proxy (optional but common) Front Maven Central and the Plugin Portal with an internal **Artifactory/Nexus** for availability, geo-locality, license/audit control, and a single chokepoint for security policy. ## Anti-patterns - Committing `caches/modules-2` to VCS. - Sharing one cache key across OS/arch (native artifacts differ). - Relying on `--refresh-dependencies` in every job (slow, network-bound, non-reproducible). - Leaving SNAPSHOTs in release pipelines and 'fixing' staleness with constant refreshes. ## Putting it together ```yaml # pseudo-CI steps: - restore-cache: key=gradle-${{ hashFiles('**/*.lockfile') }} path=~/.gradle/caches - run: ./gradlew dependencies --write-locks # only on dependency-update jobs - run: ./gradlew build --offline # fast, deterministic, no network - save-cache: key=gradle-${{ hashFiles('**/*.lockfile') }} path=~/.gradle/caches ```

  • Why key the CI cache on lockfiles rather than always using a fixed key?
    A lockfile hash changes only when dependencies actually change, so the cache is reused across most builds yet invalidated precisely when needed. A fixed key risks serving a stale cache that's missing new dependencies (breaking --offline).
  • What does dependency verification add on top of the cache?
    It records expected SHA-256/PGP for each artifact in verification-metadata.xml. On resolution Gradle rejects any artifact whose checksum/signature doesn't match — defending against a poisoned shared cache or compromised mirror.
  • Why prefer --offline over --refresh-dependencies in steady-state CI?
    --offline is fast, network-independent, and deterministic; --refresh-dependencies forces network round-trips and re-resolution on every run, hurting speed and reproducibility. Refresh belongs only in deliberate dependency-update jobs.

saying these in an interview costs you the question

  • Proposing to commit the cache to the repository.
  • Using SNAPSHOT/dynamic versions in release pipelines and compensating with constant --refresh-dependencies.
  • One global cache key shared across OS/arch and unrelated dependency sets.

context