skip to content

How does declaring a task's inputs and outputs create implicit dependency edges in the task graph?

level: middleimportance: should knowfreq 50%

answer

  1. output Provider knows its producer
  2. input.set(producer.flatMap{...}) wires edge
  3. data flow => implicit dependency
  4. no dependsOn needed
  5. manual path + dependsOn = fragile

basics

~10 s

If task B consumes a file produced as task A's declared output, Gradle infers that B depends on A and adds that edge automatically — you don't need an explicit dependsOn.

solid answer

~40 s

Gradle's `Provider`-based wiring lets one task's output become another's input, and that connection *carries* the producing task as an implicit dependency. When you declare `B.inputs` (or a typed input property) from `A.outputs`, Gradle records that the value comes from A. During graph construction it resolves these provider chains and adds an edge B → A automatically — so requesting B pulls A in and runs it first, with no explicit `dependsOn`. This is the modern, preferred wiring: it's order-safe and keeps the graph correct even if you refactor. The key API is connecting a `Provider`/`Property` from the producer's output to the consumer's input (e.g. `consumer.inputFile.set(producer.flatMap { it.outputFile })`). Hard-coding `dependsOn` plus reading a file path manually loses this inference and is a frequent source of missing-dependency bugs.

code

kotlin · 6 lines
kotlin
val producer = tasks.register<Producer>("producer")
tasks.register<Consumer>("consumer") {
    // setting the input from the producer's output provider
    // makes Gradle add an implicit 'consumer -> producer' edge
    inputFile.set(producer.flatMap { it.outputFile })
}

go deeper

for a junior

Know that consuming another task's declared output implies a dependency.

for a middle

Explain Provider/Property wiring and that input.set(provider) creates the edge.

for a senior

Contrast with the dependsOn+hardcoded-path anti-pattern and tie inputs/outputs to caching.

for a principal

Advocate provider-based wiring as a convention across a multi-module build to eliminate hidden missing-dependency classes of bugs.

## Implicit task dependencies via inputs/outputs Gradle infers graph edges from **data flow**, not just explicit declarations. The mechanism is the **`Provider`/`Property`** API. ### How it works A task output declared as a `Property<RegularFile>` or `DirectoryProperty` is a **provider** that knows which task produces it. When a *consumer* task sets one of its input properties **from** that provider, Gradle records the producer as a **producer of that value**. During graph construction, Gradle resolves the provider chain: it sees the consumer's input value originates from the producer's output, and adds an **implicit dependency edge** consumer → producer. Requesting the consumer therefore pulls the producer in and orders it first — **without any `dependsOn`**. ### Why this is preferred - **Correct by construction**: you can't forget the dependency because the data link *is* the dependency. - **Refactor-safe**: rename or move the producer and the wiring still resolves. - **Lazy**: `Provider` defers computing the value until needed, so it composes with configuration avoidance. ### The anti-pattern ```kotlin // WRONG: manual path + explicit dependsOn, easy to desync consumer { dependsOn(producer) from("build/generated/out.txt") } ``` If someone removes the `dependsOn` but keeps the path, the file may not exist yet — a flaky 'works on second run' bug. ### The correct wiring ```kotlin val producer = tasks.register<Producer>("producer") tasks.register<Consumer>("consumer") { // input provider carries 'producer' as an implicit dependency inputFile.set(producer.flatMap { it.outputFile }) } ``` Here `inputFile.set(producer.flatMap { ... })` both supplies the value **and** establishes the edge. Gradle wires `consumer -> producer` in the graph automatically. ## Relation to up-to-date checks The same declared inputs/outputs feed incremental build/up-to-date checking — declaring them properly is what makes both correct graph ordering *and* caching possible.

  • Why is provider-based wiring safer than dependsOn plus a hardcoded path?
    The provider link is both the data source and the dependency, so they can't drift apart. A hardcoded path with a manual dependsOn can desync — drop the dependsOn and you get a missing-file bug that only appears on clean builds.
  • Do implicit dependencies still benefit from up-to-date checking?
    Yes — the same declared inputs/outputs that infer the edge also feed Gradle's incremental/up-to-date logic and the build cache, so proper declaration helps ordering and avoidance together.

saying these in an interview costs you the question

  • Saying you always need an explicit dependsOn between producer and consumer.
  • Reading an output file by raw path inside a consumer without wiring the provider (loses the inferred edge).

context