skip to content

A plugin author chains providers but the build still evaluates values too early or loses task ordering. What anti-patterns break provider laziness, and how do you keep the whole chain deferred?

level: seniorimportance: must knowfreq 38%

answer

  1. terminal reads: get/getOrNull/getOrElse/isPresent
  2. string interpolation forces get
  3. get-inside-map drops dependency
  4. wire provider into task input, don't get()
  5. read concrete value only in task action

basics

~10 s

Calling get/getOrElse/getOrNull during configuration, capturing values into local variables, or unwrapping providers inside map lambdas forces eager evaluation and drops dependencies. Stay lazy with map/flatMap/zip/orElse and wire providers directly into task inputs.

solid answer

~40 s

Laziness breaks the instant you *terminally read* a provider during configuration: `get()`, `getOrElse()`, `getOrNull()`, `isPresent`, or feeding a provider into a string template (which calls `toString`/`get`). Other culprits: capturing `provider.get()` into a `val` and reusing it (snapshots and loses dependency edges), and calling `.get()` *inside* a `map` lambda to unwrap a nested provider instead of using `flatMap`. To keep a chain deferred end-to-end: derive with `map`/`flatMap`/`zip`/`orElse` only; never call terminal reads at config time; wire providers straight into task input properties with `inputProp.set(derivedProvider)` so Gradle queries them at execution and inherits producer-task dependencies automatically; and where you need a value in a task action, read it there (lazily-late) via `get()`. This preserves configuration avoidance, the configuration cache, up-to-date checks, and implicit task ordering.

code

kotlin · 6 lines
kotlin
// BAD: eager, loses dependency, may fail if unset
archiveName.set(version.get() + ".jar")
// GOOD: fully lazy, dependency-preserving
archiveName.set(version.map { "$it.jar" })
// GOOD: reach into a task output with flatMap, not map+get
inputFile.set(generateTask.flatMap { it.outputFile })

go deeper

for a junior

Recognize that calling get() during configuration is the basic mistake to avoid.

for a middle

List the terminal operations and the string-interpolation trap, and wire providers into task inputs instead.

for a senior

Diagnose lost dependencies and early evaluation, fix with map/flatMap/zip/orElse, and connect to up-to-date checking.

for a principal

Establish org-wide lazy-API conventions, enforce configuration-cache compatibility, and review plugins for eager reads.

## What 'lazy' guarantees and why it's fragile A provider chain is lazy only as long as **no one terminally resolves it before execution**. Terminal operations are `get()`, `getOrNull()`, `getOrElse()`, and `isPresent`. The moment you call one during the configuration phase you: (1) force the value to exist *now* (may fail if unset), (2) snapshot it (later changes ignored), and (3) sever the implicit task-dependency edge that providers carry. ## The common anti-patterns 1. **Terminal read at config time:** ```kotlin val name = version.get() // BAD: forces value during configuration ``` 2. **String interpolation of a provider:** `"app-${version}.jar"` calls `version.toString()`/`get`. Use `version.map { "app-$it.jar" }` instead. 3. **Unwrapping inside map:** `task.map { it.outputFile.get() }` instead of `task.flatMap { it.outputFile }` — eager and drops the dependency. 4. **Capturing into a field/local** then wiring the captured value — you wired a constant, not a provider. 5. **isPresent at config time** to branch — forces evaluation; defer the decision with `orElse` or branch inside the task action. ## Keeping the chain deferred - Compose only with lazy operators: `map`, `flatMap`, `zip`, `orElse`, plus `ListProperty.map`. - Wire providers directly into typed task input properties: ```kotlin abstract class PackageTask : DefaultTask() { @get:Input abstract val archiveName: Property<String> } val version = objects.property(String::class.java).convention("1.0.0") tasks.register<PackageTask>("package") { archiveName.set(version.map { "app-$it.jar" }) // Gradle queries it at execution } ``` - Let Gradle do the querying: when a provider is the value of an `@Input`/`@OutputFile` property, Gradle resolves it at execution time and records dependencies and up-to-dateness. - Read concrete values only inside `doLast`/`@TaskAction`, the latest possible moment. ## Why it matters at scale Eager reads are the number-one cause of configuration-cache failures and of phantom 'task X uses output of Y without declaring a dependency' problems. A disciplined lazy chain means tasks that don't run cost nothing to configure, the configuration cache can serialize the wiring, and ordering is inferred — essential for large multi-module builds.

  • Why does string interpolation of a provider break laziness?
    Interpolation calls toString()/get() on the provider, forcing eager evaluation at configuration time. Use provider.map to build the string lazily instead.
  • How does eager reading cause missing task dependencies?
    Providers carry an implicit dependency on their producer task. Calling get() reduces the provider to a plain value, discarding that edge, so the consumer no longer schedules the producer.
  • Where is it safe to call get()?
    Inside a task action (@TaskAction/doLast), at execution time — the latest possible moment — where the value is genuinely needed and all producers have run.
  • How does this relate to the configuration cache?
    Eager config-time reads bypass the serialized lazy wiring; staying lazy lets Gradle store and replay the provider graph, which the configuration cache requires.

A provider chain is like a recipe; calling get() at config time is cooking the dish in the kitchen-design phase — you can't re-plate it later and you forget who supplied the ingredients.

saying these in an interview costs you the question

  • Recommending get()/getOrElse() during configuration to 'simplify' wiring.
  • Using string interpolation on providers in build logic.
  • Unwrapping nested providers with get() inside map instead of flatMap.
  • Branching on isPresent at configuration time.

context