Why do dynamic and changing versions threaten build reproducibility, and what mechanisms does Gradle offer to control or forbid them?
answer
- same commit → different artifacts = non-reproducible
- dependencyLocking + gradle.lockfile + --write-locks
- failOnDynamicVersions / failOnChangingVersions
- failOnNonReproducibleResolution
- cache windows ≠ reproducibility
basics
~10 sThey make the same source resolve different artifacts over time, so builds aren't reproducible. Gradle counters this with dependency locking (gradle.lockfile) and resolutionStrategy guards like failOnDynamicVersions() / failOnChangingVersions().
solid answer
~50 sDynamic selectors and changing modules resolve against *what is published at build time*, so two builds of the same commit can pull different bytes — breaking reproducibility, audit, and 'works on my machine' debugging. Gradle's controls: - **Dependency locking** — enable `dependencyLocking { lockAllConfigurations() }`, resolve once with `--write-locks`, and Gradle pins the resolved set into `gradle.lockfile`. Subsequent builds verify against it and fail on drift. This is the primary reproducibility tool for projects that *do* use dynamic/changing versions. - **Fail-fast guards** — `resolutionStrategy.failOnDynamicVersions()`, `failOnChangingVersions()`, or the combined `failOnNonReproducibleResolution()` make a build error out if such inputs are present at all. - **Tight cache windows** — `cacheDynamicVersionsFor` / `cacheChangingModulesFor` reduce drift between refreshes but don't eliminate it. The usual policy: allow dynamic/changing for fast internal iteration, but for releases either ban them with fail-fast guards or pin them with locking so the resolved graph is auditable and deterministic.
code
kotlin · 10 linesdependencyLocking {
lockAllConfigurations()
}
configurations.all {
resolutionStrategy {
failOnDynamicVersions()
failOnChangingVersions()
}
}
// upgrade flow: ./gradlew dependencies --write-locks --refresh-dependenciesgo deeper
Know that dynamic/changing deps can make builds differ over time and that lockfiles exist.
Describe the locking workflow (lockAllConfigurations, --write-locks, gradle.lockfile) at a usage level.
Compare locking vs fail-fast guards, explain the upgrade flow, and why cache windows aren't determinism.
Define org policy: catalogs/platforms for static versions, locking with committed lockfiles, fail-fast guards in release modules, and a reviewed upgrade cadence for supply-chain auditability.
## The reproducibility problem A reproducible build maps a given source revision to the same outputs. Dynamic versions (`1.+`, `latest.release`) and changing modules (`-SNAPSHOT`, `changing=true`) break this because resolution reads live repository state: a new matching release or a re-published snapshot changes the inputs without any source change. Consequences: non-deterministic CI, hard-to-reproduce bugs, and a fuzzy supply-chain record. ## Dependency locking Locking captures the *resolved* versions of dynamic/changing dependencies into a lockfile so future builds are pinned. ```kotlin dependencyLocking { lockAllConfigurations() } ``` Workflow: 1. `./gradlew dependencies --write-locks` (or any task with `--write-locks`) resolves and writes `gradle.lockfile` per project. 2. Normal builds read the lockfile and **fail** if resolution would differ from the locked set. 3. To upgrade, run `--write-locks` again (often with `--refresh-dependencies` to re-check newer candidates), review the diff, and commit it. Locking is what makes dynamic versions safe in practice: you get the convenience of `latest.release` plus a pinned, reviewable record. You can lock all configurations or a subset, and use lock modes (`STRICT`, `LENIENT`). ## Fail-fast guards If a project should never contain non-reproducible inputs, fail the build instead: ```kotlin configurations.all { resolutionStrategy { failOnDynamicVersions() failOnChangingVersions() // or both at once: // failOnNonReproducibleResolution() } } ``` These turn an accidental `1.+` or stray `-SNAPSHOT` into a hard error — useful as a guardrail in release modules and shared platforms. ## Cache windows are not reproducibility Shortening `cacheDynamicVersionsFor`/`cacheChangingModulesFor` only changes *how often* drift can happen, not *whether* it can. They are a freshness knob, not a determinism guarantee. Don't present them as a reproducibility solution. ## Recommended layering 1. Prefer static versions; centralise them in a version catalog / platform. 2. Where dynamic/changing is genuinely needed (internal libs, fast iteration), enable **locking** and commit lockfiles. 3. In release-critical modules, add **fail-fast guards** so nothing slips through. 4. Reserve `--refresh-dependencies --write-locks` for deliberate, reviewed upgrades.
- If locking is enabled, can you still use 'latest.release'?Yes. Locking pins whatever latest.release resolved to; you keep the selector but builds are reproducible. You bump it intentionally with --write-locks.
- What does failOnNonReproducibleResolution() add over the two specific guards?It's a convenience that activates both dynamic and changing failures together (and future non-reproducible categories), rather than declaring each individually.
- Does shortening the dynamic-versions cache make the build reproducible?No. It only changes how often drift can occur; reproducibility requires locking or static versions.
Dynamic/changing deps are like citing 'today's newspaper' in a contract — accurate when written, ambiguous later. A lockfile is photocopying the exact edition and stapling it to the contract.
saying these in an interview costs you the question
- Claiming short cache TTLs make builds reproducible.
- Saying locking forbids dynamic versions (it pins them, doesn't ban them).
- Recommending --refresh-dependencies as a reproducibility tool (it's the opposite).