Why is wiring extension providers into tasks (rather than capturing values via closures or eager reads) essential for configuration-cache compatibility and reliable up-to-date checks?
answer
- config cache serializes task graph
- no Project/extension refs at execution
- wired provider = serializable + tracked
- closure over project = cache failure
- eager copy = untracked input, stale up-to-date
basics
~10 sProvider wiring keeps task inputs as serializable, declared lazy values resolved at execution. Closures over the project/extension capture live objects the configuration cache can't serialize, and eager reads bypass input tracking, breaking up-to-date checks.
solid answer
~50 sThe configuration cache serializes the task graph after configuration and reuses it on later builds, skipping configuration entirely. For that to work, a task's state must be made of serializable, self-contained values — not references to the `Project`, the extension object, or closures that read them at execution. When you wire `task.input.set(extension.input)`, Gradle stores the resolved provider chain as part of the task's serializable state and resolves it at execution against captured inputs; nothing reaches back into the live build model. By contrast, a `@TaskAction` (or a `doLast` closure) that calls `project.extensions.getByType(...)` at execution captures the `Project`, which the configuration cache forbids. Eager reads cause a subtler bug: copying `extension.x.get()` into a plain field means the value isn't a tracked `@Input` provider, so Gradle can't detect when it changes — up-to-date checks and the build cache key go stale. Provider wiring makes inputs both *serializable* and *tracked*.
code
kotlin · 9 linesabstract class PrintTask : DefaultTask() {
@get:Input abstract val message: Property<String>
@TaskAction fun run() = println(message.get()) // reads only own property
}
// plugin wires provider -> property; action never touches project/extension
tasks.register("print", PrintTask::class.java) {
it.message.set(ext.message) // serializable, tracked, config-cache safe
}go deeper
Know that tasks should read their own wired properties, not the project.
Explain that closures over the project break the config cache and that eager copies aren't tracked inputs.
Connect provider wiring to both serializability and input fingerprinting for up-to-date and build-cache correctness.
Set authoring standards (annotated lazy inputs, no live-model access in actions) so the whole build is config-cache clean and cacheable.
## What the configuration cache requires The **configuration cache** persists the configured task graph and replays it on subsequent builds, skipping the configuration phase. To serialize a task, Gradle must turn its state into bytes — so every field that matters must be serializable and free of references to the live build model (`Project`, `Gradle`, `Configuration`, `Task` instances, scripts). Holding those at execution time is an error the cache reports. ## How provider wiring satisfies this When you do: ```kotlin task.message.set(extension.message) ``` you attach a `Provider` to the task's `Property`. Gradle knows how to serialize provider chains built from other properties and standard sources. At execution the provider is resolved from the *serialized* inputs, not by phoning back into the project. So the task is self-contained and cacheable. ## The two anti-patterns **1. Closures that capture the build model.** Reading config at execution: ```kotlin // BAD: captures Project, fails config cache doLast { println(project.extensions.getByType(Ext::class.java).message.get()) } ``` The lambda closes over `project`. The configuration cache can't serialize that, so the build errors (or silently behaves wrong if checks are off). **2. Eager reads into untracked fields.** Copying a resolved value: ```kotlin // BAD: value isn't a tracked @Input provider val msg = extension.message.get() task.doLast { println(msg) } ``` Here `msg` is a captured constant. It bypasses Gradle's input tracking: it's not an `@Input Property`, so Gradle doesn't know it's part of the task's fingerprint. **Up-to-date checks** and the **build cache key** are computed from declared inputs/outputs; an untracked captured value won't invalidate them when it changes, producing stale results. ## Why the lazy wiring fixes both A wired `@Input Property`: - is **serializable** as a provider chain → config-cache safe; - is a **declared input** → its value participates in up-to-date and build-cache fingerprints; - resolves at execution from serialized state → no live model access. ## Rule of thumb Expose task inputs as `@Input`/`@InputFiles` lazy properties, wire them from extension providers in the plugin, and let the `@TaskAction` read only `this`'s own properties — never `project` or the extension directly.
- What goes wrong if a `doLast` block calls `project.extensions.getByType(...)` at execution?It captures the live `Project`, which the configuration cache cannot serialize, so the build fails the cache check (or behaves incorrectly). Read from the task's own wired properties instead.
- Why does an eagerly captured value break up-to-date checks?It isn't a declared `@Input` provider, so Gradle's input fingerprint doesn't include it. When that value changes, the task still looks up-to-date and produces stale output.
- Does provider wiring alone guarantee correct up-to-date behavior?Only if the property is annotated as a tracked input/output (`@Input`, `@InputFile`, etc.). Wiring plus correct annotations together make the value participate in the fingerprint.
saying these in an interview costs you the question
- Suggesting the task action read the extension via `project.extensions` at execution.
- Claiming eager value capture is fine as long as you 'read it once'.
- Believing the configuration cache can serialize closures over `Project`.