skip to content

Engineers sometimes read a project extension property at the top of an eachDependency rule and are surprised by the value or by configuration cache warnings. Explain the configuration-time vs resolve-time subtlety and how to capture inputs safely.

level: seniorimportance: should knowfreq 25%

answer

  1. register at config time, body at resolve time
  2. don't capture Project in the closure
  3. config cache flags live captures
  4. capture a val/Provider up front
  5. body runs once per module — keep it cheap

basics

~20 s

The eachDependency closure runs at resolve time, but you register it at configuration time. Capturing live project state inside it can read stale values or break the configuration cache. Resolve inputs into a Provider/local val up front instead.

solid answer

~40 s

`eachDependency` registration happens during configuration; the closure body executes later, at resolve time. If inside the closure you reach back into mutable project state (e.g. `project.extensions...`, a `var` that changes), you can read a different value than expected, and with the **configuration cache** you can hit problems because the closure must not capture the `Project` or other non-serializable live objects across the cache boundary. The fix is to compute your inputs **before** registering — read the version into a local `val` or, better, a lazy `Provider`/`Property` (`providers.gradleProperty(...)`, an extension `Property<String>`), then reference only that captured value inside the closure. This keeps the rule deterministic and configuration-cache-friendly. Also keep the body cheap: it runs once per module per resolution, so avoid I/O or heavy computation there.

code

kotlin · 11 lines
kotlin
// GOOD: capture a serializable Provider, not the Project
val guavaVersion = providers.gradleProperty("guavaVersion").orElse("33.2.1-jre")

configurations.all {
    resolutionStrategy.eachDependency {
        if (requested.name == "guava") {
            useVersion(guavaVersion.get())
            because("centralized version")
        }
    }
}

go deeper

for a junior

Recall that the rule runs later, at resolve time, not when the script is read.

for a middle

Explain the stale-state risk and that you should compute inputs before the closure.

for a senior

Tie it to the configuration cache: avoid capturing Project, use Provider/val, keep the body cheap.

for a principal

Set conventions so shared convention plugins register such rules cache-safely across the org and avoid per-module overhead.

## Two phases, one closure Gradle separates **configuration time** (scripts run, tasks/configurations get defined) from **resolve/execution time** (graphs resolve, tasks run). When you write: ```kotlin configurations.all { resolutionStrategy.eachDependency { /* body */ } } ``` the *registration* runs at configuration time, but the **body runs later**, when each configuration is resolved. Anything the body reads is read *then*, not now. ## The stale-state trap If the body reads mutable project state, the value it sees is whatever that state holds at resolve time — which may differ from what you assumed when writing the script, especially if other plugins mutate it. Worse, reaching into live objects ties the rule to runtime project state instead of a fixed input. ## The configuration cache angle The **configuration cache** serializes the configured build (including registered rules) and replays it, skipping configuration on cache hits. Closures that capture non-serializable, live objects — notably `Project`, or services reached through it — are flagged as problems because they can't be safely stored/replayed. A rule that pulls `project.findProperty(...)` from inside the closure is exactly this kind of capture. ## The safe pattern: capture inputs up front Resolve your inputs **before** registering, into an immutable local or a lazy `Provider`: ```kotlin val jacksonVersion: Provider<String> = providers.gradleProperty("jacksonVersion").orElse("2.17.1") configurations.all { resolutionStrategy.eachDependency { if (requested.group == "com.fasterxml.jackson.core") { useVersion(jacksonVersion.get()) // captured Provider, no Project capture because("centralized Jackson version") } } } ``` The closure now closes over `jacksonVersion` (a `Provider`, which the configuration cache can handle) instead of the `Project`. `providers.gradleProperty`, an extension `Property<String>`, or a plain captured `val` all work — the point is the value is fixed/lazy and serializable, not a live reach-back into `project`. ## Performance note The body executes **once per module per resolution** — potentially hundreds of times. Do not perform file I/O, network calls, or expensive computation inside it. Compute once outside; reference the result inside. ## Summary - Register at configuration time, executes at resolve time. - Don't capture `Project`/mutable live state in the closure. - Capture a `val` or `Provider` of your input before registering. - Keep the body cheap and deterministic.

  • Why does capturing project.findProperty(...) inside the closure trigger configuration-cache problems?
    It captures the live Project (or services reached through it), which is non-serializable across the cache boundary. Capture a Provider or local val instead.
  • Why must the closure body be cheap?
    It runs once per dependency per resolution — possibly hundreds of times — so I/O or heavy work inside it multiplies and slows every resolve.
  • What types can the closure safely close over for the configuration cache?
    Serializable values: a captured immutable val, or a lazy Provider/Property obtained from providers/extensions — not the Project itself.

It's like writing instructions for a worker to follow tomorrow: bake the constant numbers into the note now, instead of telling them to phone you back tomorrow to ask what the number is.

saying these in an interview costs you the question

  • Saying the closure runs at configuration time so inputs are fixed then.
  • Claiming capturing Project is fine because it always works without the configuration cache.

context