Why does Gradle resolve Provider/Property values lazily at execution time, and what problems does eager resolution cause?
answer
- config phase vs execution phase
- value = recipe, resolved on get()
- no work for un-run tasks
- auto dependency inference from wired outputs
- required for configuration cache
basics
~20 sLazy resolution means a value is computed only when read (usually at execution), not when configured. This avoids wasted work for tasks that don't run, lets a value depend on results produced later, and keeps the build configuration-cache friendly.
solid answer
~50 sGradle's build has a **configuration phase** (every build script and plugin runs to set up tasks) and an **execution phase** (only the requested tasks run). Eagerly computing values during configuration means you pay for them on *every* invocation even if the task never runs — and you may compute them before other inputs are finalized. `Provider`/`Property` defer the computation: the value is calculated only when `get()` is called, typically inside a task action during execution. This enables three things: (1) **performance** — no work for un-executed tasks and cheaper configuration; (2) **correct wiring** — a `Property` can be `set(anotherTask.output)` and resolve after that task runs, so Gradle infers the task dependency automatically; (3) **configuration cache** — cached configuration must capture values as providers, not as already-resolved fields. Eager `get()` in `apply()` or a configuration block defeats all three.
code
kotlin · 6 lines// Wiring one task's output to another's input — resolved lazily.
val producer = tasks.register<WriteFile>("producer")
val consumer = tasks.register<ReadFile>("consumer") {
inputText.set(producer.flatMap { it.outputText }) // dependency inferred
}
// consumer.inputText resolves AFTER producer runs, at execution timego deeper
Know that values are computed when read, not when configured.
Explain the config-vs-execution phases and that reading should happen in the task action.
Connect deferral to performance, automatic dependency inference, and configuration-cache requirements; identify eager-get() bugs.
Drive adoption of lazy wiring across many modules to unlock config-cache and reduce configuration-time cost build-wide, and review plugins for eager-resolution regressions.
## Two phases of a Gradle build 1. **Configuration phase** — Gradle evaluates the settings + every project's build script and runs all applied plugins. The whole task graph is *configured* here, regardless of what you asked to run. 2. **Execution phase** — only the tasks needed for the requested goals actually execute their actions. If you compute a value during configuration (e.g., read a file, query a system property, concatenate strings into a field), you pay that cost on **every** build, even `./gradlew help`, and even for tasks that won't run. ## How lazy types defer A `Provider<T>` holds a **recipe** for the value, not the value itself. The recipe runs when something calls `get()`/`getOrElse()`. If you wire `myTask.input.set(otherTask.outputProvider)`, the recipe is "read otherTask's output" and it executes during the execution phase — after `otherTask` has produced it. ## Three concrete benefits - **Avoid wasted work:** a value backing a task that isn't in the requested graph is never computed. - **Automatic dependency inference:** when a `Property` is set from another task's output `Provider`, Gradle records the producing task as a dependency. No manual `dependsOn` needed. - **Configuration cache:** Gradle can serialize the configured task graph and skip the configuration phase on later builds. This only works if inputs are captured as serializable `Provider`s; an eagerly-resolved field may capture stale or non-serializable state. ## The classic eager-resolution bug ```kotlin // BAD: resolves during configuration val version = project.providers.gradleProperty("appVersion").get() tasks.register("stamp") { doLast { println(version) } } ``` Here `.get()` runs at configuration time, every build, and reads the property before later configuration could change it. The fix is to keep it a `Provider` and read inside the action: ```kotlin val version = project.providers.gradleProperty("appVersion") tasks.register("stamp") { doLast { println(version.get()) } } ``` ## Mental model Think of providers as forming a small lazy graph of computations that Gradle evaluates *at the latest responsible moment*. The discipline is: declare and wire during configuration, **read during execution**.
- How does lazy wiring let Gradle infer a task dependency without dependsOn?When a Property is set from a Provider that originates in another task's output, Gradle records that producing task as a dependency of any task consuming the value. Resolving the input at execution forces the producer to run first.
- Give one way eager resolution breaks the configuration cache.The configuration cache serializes the task graph including input providers. An eagerly-resolved field may capture non-serializable or environment-specific state at configuration time, or miss later changes, so Gradle can't safely replay it; keeping values as providers avoids this.
It's like a restaurant order ticket: you write down what to make (configure) but the dish is only cooked when the table actually orders it (execution) — you don't cook every possible meal up front.
saying these in an interview costs you the question
- Claiming lazy resolution is only a micro-optimization with no correctness role.
- Saying configuration and execution are the same phase.
- Recommending .get() in apply()/configuration to 'simplify' code.