skip to content

A teammate's plugin reads `extension.outputDir.get()` inside the plugin's `apply` method to configure a task, and reports that the value users set in the build script is being ignored. Diagnose the bug and fix it.

level: middleimportance: must knowfreq 55%

answer

  1. apply runs before user DSL
  2. .get() snapshots default
  3. set(provider) restores live link
  4. map/flatMap to transform lazily
  5. get() in plugin = smell

basics

~10 s

.get() resolves the value during configuration, before the user's DSL block runs, so it captures the default. Fix it by wiring the provider lazily: task.outputDir.set(extension.outputDir) with no .get().

solid answer

~50 s

The plugin's `apply` runs early in project configuration; the user's `extension { outputDir = ... }` block runs afterward. Calling `extension.outputDir.get()` in `apply` forces the value *at that point*, which is the default (or unset), so the user's later assignment never reaches the task. The fix is to pass the provider through instead of resolving it: `task.outputDir.set(extension.outputDir)`. Now the task's property holds a live link back to the extension, and Gradle resolves it lazily — at execution, when the user's value is in place. If you must transform the value, use provider operators like `extension.outputDir.map { ... }` so the transformation also stays lazy. The rule of thumb: in plugin/configuration code, never call `get()` on a value you intend to wire into a task; only Gradle should call `get()`, and it does so at the right time.

code

kotlin · 8 lines
kotlin
// BUG: eager read during apply
task.outputDir.set(extension.outputDir.get()) // captures default

// FIX: lazy provider wiring
task.outputDir.set(extension.outputDir)

// FIX with transform, still lazy
task.reportFile.set(extension.outputDir.map { it.file("report.txt") })

go deeper

for a junior

Recognize that .get() in apply is the bug and that set(provider) is the fix.

for a middle

Explain the configuration ordering precisely and how map/flatMap keep transforms lazy.

for a senior

Generalize to a 'get() is a smell in plugin code' rule and reason about who may resolve values.

for a principal

Tie eager reads to broken configuration-cache compatibility and unreliable builds at scale.

## Configuration timing in Gradle A project is configured top to bottom. Plugins are applied (their `apply` methods run) when `plugins { }` / `apply plugin:` is processed, which is **before** the body of the build script that configures those plugins' extensions. So the lifecycle is: 1. `apply` creates the extension (defaults in place) and registers tasks. 2. The build script's `myExt { outputDir = file("out") }` block runs, mutating the extension. 3. At execution, tasks read their inputs. ## The eager-read bug If step 1 does `task.outputDir = extension.outputDir.get()`, it reads the value at the end of step 1 — before step 2 has run. The user's assignment in step 2 lands on the extension property, but the task already copied the *old* value. The task therefore uses the default, and the user concludes their configuration is "ignored." ## The fix: keep it lazy Replace the eager copy with a provider wiring: ```kotlin task.outputDir.set(extension.outputDir) ``` `set(Provider)` stores the link, not the value. When Gradle reads `task.outputDir` at execution, it walks back to `extension.outputDir`, which now reflects the user's step-2 assignment. ## Transforming while staying lazy Sometimes you don't want the raw extension value but a derived one. Don't `get()` then transform — chain provider operators: - `provider.map { transform(it) }` — lazily transform a present value. - `provider.flatMap { otherProvider }` — chain to another provider. - `provider.orElse(default)` — supply a fallback lazily. ```kotlin task.reportFile.set(extension.outputDir.map { it.file("report.txt") }) ``` Everything stays deferred, so the user's later changes still flow through. ## Heuristic In plugin code, treat `get()` as a smell. The only legitimate `get()` callers are Gradle itself (at execution) and tests. If you find yourself resolving a value to wire it somewhere, you've broken laziness.

  • How would you wire a *derived* value (extension dir + a fixed filename) without breaking laziness?
    Use a provider operator: `extension.outputDir.map { it.file("report.txt") }`, then `set` that provider into the task. `map` defers the transform until resolution.
  • Who is allowed to call `.get()` legitimately?
    Gradle itself at task execution, and test code. Plugin/configuration code should pass providers through rather than resolving them.

saying these in an interview costs you the question

  • Claiming the fix is to move the user's DSL block above the plugin apply.
  • Resolving the value with `get()` and then transforming it eagerly.

context