skip to content

What is an inferred (implicit) task dependency, and how does wiring a producer's output Provider into a consumer's input property make explicit `dependsOn` unnecessary?

level: middleimportance: must knowfreq 70%

answer

  1. output Provider carries its producer task
  2. wire output -> input = auto dependency
  3. flatMap keeps it lazy
  4. no manual dependsOn needed
  5. same graph as explicit edges

basics

~20 s

If you connect a producing task's output Provider (e.g. its RegularFileProperty) directly to a consuming task's input property, Gradle sees the output 'carries' its producing task and automatically adds the dependency — no manual dependsOn needed.

solid answer

~40 s

Modern Gradle tracks **task dependencies through the file-property graph**. A task's `@OutputFile`/`@OutputDirectory` is a `Provider` (e.g. `RegularFileProperty`) that internally knows the task that produces it. When you assign that Provider into another task's `@InputFile` property — e.g. `consumer.inputFile.set(producer.flatMap { it.outputFile })` — the input property *carries* the producer task as an implicit dependency. So when Gradle resolves the consumer's inputs to build the graph, it discovers the producer and schedules it first **automatically**. This is the idiomatic, lazy approach: it works with `register` (configuration avoidance), survives task reordering, and keeps dependencies in sync with the actual data flow. You avoid both the boilerplate and the bugs of hand-written `dependsOn` (forgetting an edge, or a stale edge after refactor). Manual `dependsOn` is reserved for cases with no file-property data flow to wire.

code

kotlin · 7 lines
kotlin
val generate = tasks.register<GenerateTask>("generate") {
    outputFile.set(layout.buildDirectory.file("gen/out.txt"))
}
val consume = tasks.register<ConsumeTask>("consume") {
    // No dependsOn(generate) — wiring the Provider infers it.
    inputFile.set(generate.flatMap { it.outputFile })
}

go deeper

for a junior

Recognize that connecting an output to an input can make Gradle run the producer first automatically.

for a middle

Explain that output Providers carry their producing task and that flatMap keeps wiring lazy; show the producer/consumer snippet.

for a senior

Argue why inferred dependencies are more robust than manual dependsOn (data-flow-derived, refactor-safe, ties into incrementality/caching).

for a principal

Position provider-wiring as the standard for a large build to eliminate hand-maintained edges and guarantee correct graphs at scale.

## The core idea Gradle's lazy `Provider`/`Property` API does more than defer computation — output file properties are **task-aware**. A `RegularFileProperty` produced by a task records that task as its producer. When such a Provider flows into another task's input, Gradle treats the producing task as an *inferred dependency* of the consumer. ## Producer task ```kotlin abstract class GenerateTask : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun run() = outputFile.get().asFile.writeText("generated") } val generate = tasks.register<GenerateTask>("generate") { outputFile.set(layout.buildDirectory.file("gen/out.txt")) } ``` ## Consumer task — no dependsOn ```kotlin abstract class ConsumeTask : DefaultTask() { @get:InputFile abstract val inputFile: RegularFileProperty @TaskAction fun run() = println(inputFile.get().asFile.readText()) } val consume = tasks.register<ConsumeTask>("consume") { // Wire producer output -> consumer input. flatMap keeps it lazy. inputFile.set(generate.flatMap { it.outputFile }) } ``` Running `./gradlew consume` runs `generate` first — **without any `dependsOn`**. The `inputFile` property carries `generate` as its producer. ## Why this beats manual dependsOn - **Correctness:** the dependency is derived from the actual data flow, so it can't drift out of sync when you rename/move tasks. - **Laziness:** works with configuration avoidance (`register`, `flatMap`, `map`); the producer is only configured if actually needed. - **Up-to-date checking:** the same wiring makes the file an `@InputFile`, so the consumer participates correctly in incremental builds and caching. ## How Gradle implements it Internally, output properties expose a `TaskDependency` (their `producer`). During graph construction Gradle calls `Task.getTaskDependencies()` and the resolution of each input `Provider` contributes the producer task. This is the same `TaskDependencyContainer` mechanism that `dependsOn` feeds into — inferred edges and explicit edges end up in the same graph. ## When you still need dependsOn If there is no file/property data flow (e.g. one task must run after another purely for side effects with no shared artifact), there's nothing to wire, so an explicit `dependsOn` (or an ordering hook) is the correct tool.

  • Why use `flatMap` instead of `generate.get().outputFile`?
    `flatMap` keeps the wiring lazy and avoids eagerly realizing the `generate` task at configuration time, preserving configuration avoidance; `get()` would force it.
  • Does the inferred dependency also help up-to-date checking?
    Yes — the same wiring registers the file as an `@InputFile`, so the consumer's up-to-date and cache key correctly reflect the producer's output.

saying these in an interview costs you the question

  • Saying you 'must always add dependsOn' even when output→input is wired — that's redundant.
  • Calling `.get()` on the producer at configuration time, breaking laziness and inference.

context