skip to content

How does Gradle decide when a cached SNAPSHOT or dynamic version is stale, and how can you control that freshness window?

level: seniorimportance: should knowfreq 35%

answer

  1. immutable vs changing vs dynamic
  2. default TTL 24h
  3. cacheChangingModulesFor / cacheDynamicVersionsFor
  4. isChanging = true
  5. --refresh-dependencies = one-shot override

basics

~10 s

Gradle caches changing modules (SNAPSHOTs) and dynamic version listings with a time-to-live, 24 hours by default. You tune it per-configuration with resolutionStrategy.cacheChangingModulesFor and cacheDynamicVersionsFor, or override once with --refresh-dependencies.

solid answer

~40 s

Gradle distinguishes **immutable** coordinates (fixed releases like `1.4.2`) from **mutable** ones. Mutable resolution has two flavors: **changing modules** (whose content can change under a fixed coordinate — `-SNAPSHOT`, or anything marked `isChanging = true`) and **dynamic versions** (`1.+`, `latest.release`, ranges — where the *selected* version can change). For both, Gradle caches the resolved answer with a **time-to-live, default 24 hours**, to avoid hitting the network every build. Within the TTL it answers from cache; after expiry it re-checks. You control this per configuration via `resolutionStrategy`: `cacheChangingModulesFor(n, unit)` for SNAPSHOTs and `cacheDynamicVersionsFor(n, unit)` for dynamic selectors — setting them to `0` means always re-check. `--refresh-dependencies` is the one-shot CLI override that ignores the TTL for a single build. Fixed releases bypass all of this because they're assumed never to change.

code

kotlin · 10 lines
kotlin
configurations.all {
    resolutionStrategy {
        cacheChangingModulesFor(0, "seconds")   // always recheck SNAPSHOTs
        cacheDynamicVersionsFor(10, "minutes")  // dynamic selectors
    }
}

dependencies {
    implementation("com.acme:lib:1.0") { isChanging = true }
}

go deeper

for a junior

Awareness that SNAPSHOTs are cached for a while and a flag exists to refresh them.

for a middle

Name the 24h default and the two cacheXxxFor settings vs. --refresh-dependencies.

for a senior

Distinguish changing modules from dynamic versions, configure per-configuration TTLs, and reason about reproducibility tradeoffs.

for a principal

Set org policy: ban SNAPSHOT/dynamic versions in releasable builds, mandate dependency locking, and define refresh policy only for integration pipelines.

## Immutable vs. mutable resolution Gradle treats most dependency coordinates as **immutable**: `com.acme:lib:1.4.2` always means the same bytes, so once cached it's reused forever with no network check. Performance and reproducibility both benefit. Two categories break that assumption: - **Changing modules** — the coordinate is fixed but its content can be republished. The canonical case is a Maven `-SNAPSHOT`. You can also force it on any dependency: `isChanging = true`. - **Dynamic versions** — the *version selector* is open-ended: `1.+`, `latest.release`, `[1.0,2.0)`. The concrete version chosen depends on what's currently published. ## The freshness TTL For both mutable categories, Gradle caches the resolution result with a **time-to-live (default 24 hours)**. During that window it serves the cached answer without contacting the repository. After the TTL elapses, the next resolution re-checks the repo (re-listing versions for dynamic selectors, re-fetching metadata + checksum for changing modules). ## Controlling the window Configure it in the build, per configuration (or all): ```kotlin configurations.all { resolutionStrategy { // Re-check SNAPSHOTs on every build cacheChangingModulesFor(0, "seconds") // Re-resolve dynamic versions at most every 10 minutes cacheDynamicVersionsFor(10, "minutes") } } ``` Mark an arbitrary dependency as changing: ```kotlin dependencies { implementation("com.acme:lib:1.0") { isChanging = true } } ``` ## CLI override `--refresh-dependencies` ignores the TTL for one build, forcing a re-check of all metadata. It's the imperative counterpart to the declarative `cacheXxxFor` settings. ## Practical guidance - For **reproducible release builds**, avoid SNAPSHOTs and dynamic versions entirely (or lock them with dependency locking) so the TTL never matters. - For **integration pipelines** that must pull the newest snapshot, set `cacheChangingModulesFor(0, ...)` or run `--refresh-dependencies` in that job. - Setting TTLs to 0 trades reproducibility and build speed for freshness — use deliberately.

  • What's the difference between a changing module and a dynamic version?
    A changing module has a fixed coordinate whose *content* can be republished (e.g. -SNAPSHOT or isChanging=true). A dynamic version has an open-ended selector (1.+, latest.release, ranges) where the *selected version* can change. Different cache controls apply to each.
  • What is the default cache TTL for these mutable dependencies?
    24 hours, for both changing modules and dynamic version listings, unless overridden via resolutionStrategy or --refresh-dependencies.
  • How would you make a release build immune to this freshness concern entirely?
    Use only fixed release versions and enable dependency locking so even transitive dynamic versions resolve to pinned values. Then the TTL is irrelevant because nothing is mutable.

saying these in an interview costs you the question

  • Saying fixed release versions are also subject to a TTL — they're treated as immutable.
  • Conflating changing modules with dynamic versions; they have separate cache settings.
  • Believing the default TTL is zero (always re-check) — it's 24 hours.

context