skip to content

What does it mean for a task dependency to be "inferred" in Gradle, and how does it differ from calling dependsOn?

level: middleimportance: must knowfreq 60%

answer

  1. value + ordering travel together
  2. Property set from producer's Provider
  3. annotated input → Gradle scans backing provider
  4. no dependsOn string
  5. .get() severs the link

basics

~10 s

If one task's input is wired to another task's output Provider, Gradle automatically runs the producer first. You never call dependsOn — the dependency is implied by the data wiring itself.

solid answer

~40 s

An inferred (or implicit) task dependency is one Gradle derives from data flow rather than a manual declaration. When you connect a consumer task's input property to a producer task's output `Provider` — for example `consumer.inputFile.set(producer.outputFile)` — Gradle records that the input *carries* a producing task. At execution time, before running the consumer, it resolves the provider, sees it is backed by `producer`, and schedules `producer` first. This is preferable to `dependsOn("producer")` because the ordering and the value travel together: you can't accidentally wire the file without the dependency, and refactoring the producer's output location doesn't break the wiring. It only works when both sides use the Provider API (`Property`/`RegularFileProperty`/`DirectoryProperty`) and the input is annotated (e.g. `@InputFile`).

code

kotlin · 17 lines
kotlin
abstract class Produce : DefaultTask() {
    @get:OutputFile
    abstract val out: RegularFileProperty
    @TaskAction fun run() = out.get().asFile.writeText("data")
}
abstract class Consume : DefaultTask() {
    @get:InputFile
    abstract val src: RegularFileProperty
    @TaskAction fun run() = println(src.get().asFile.readText())
}
val producer = tasks.register<Produce>("produce") {
    out.set(layout.buildDirectory.file("gen/data.txt"))
}
tasks.register<Consume>("consume") {
    // wiring the provider also wires the dependency — no dependsOn needed
    src.set(producer.flatMap { it.out })
}

go deeper

for a junior

Know that wiring one task's output provider into another's input makes Gradle run them in order automatically, without dependsOn.

for a middle

Explain the mechanics: annotated input property holding a producer-backed provider; Gradle scans inputs and adds the edge; .get() breaks it.

for a senior

Discuss why coupling value and ordering eliminates a whole bug class, and when you still legitimately need dependsOn (pure lifecycle tasks).

for a principal

Frame inference as the basis for a reliable, refactor-safe task graph across a large multi-module build, and set conventions discouraging dependsOn-by-string.

## The idea Gradle's modern task model separates two things that used to be tangled: **what value a task needs** and **which task produces it**. With the Provider API, wiring the value *also* wires the dependency — Gradle *infers* the `dependsOn` for you. ## How it works mechanically A producer task exposes an output as a `Provider`/`Property` (e.g. `RegularFileProperty`). A producer's output property is *task-aware*: it knows which task it belongs to. When you do: ```kotlin consumer.inputFile.set(producer.flatMap { it.outputFile }) ``` the consumer's `inputFile` now holds a provider whose *producer* is the `producer` task. Gradle calls this a **task dependency carried by the provider**. During task-graph construction, Gradle walks each task's annotated inputs (`@InputFile`, `@InputFiles`, `@Nested`, …), asks each backing provider "who produces you?", and adds an edge. No `dependsOn` string is ever written. ## Why it's better than dependsOn - **Value + ordering are coupled.** You cannot wire the file without also wiring the dependency, so the classic bug "I read the file but forgot to depend on the task that writes it" disappears. - **Refactor-safe.** If the producer changes its output path, consumers keep working — they hold a provider, not a hardcoded path. - **Lazy.** The provider is only resolved at execution time, so it composes with configuration avoidance (`tasks.register`). ## Requirements for inference to fire 1. The producer output must be a real `Property`/`FileSystemLocationProperty`, ideally derived from `project.layout.buildDirectory`. 2. The consumer input must be a `Property` set from the producer's provider (use `map`/`flatMap`, never `.get()`). 3. The consumer's input must be **annotated** (`@InputFile`, etc.) so Gradle scans it. If you call `.get()` on the provider while configuring, you extract a plain value, sever the producer link, and inference is lost — you'd then need an explicit `dependsOn`. ## Contrast with dependsOn `dependsOn("compileJava")` declares *only* ordering — no value flows. Inferred dependencies declare ordering *as a side effect of* declaring data flow. Prefer inference; reserve `dependsOn` for lifecycle/aggregator tasks (like `check`) that genuinely have no data to pass.

  • What happens to inference if you call producer.get().out.get() while configuring the consumer?
    You eagerly resolve to a plain RegularFile, dropping the producer's task-dependency metadata. Gradle no longer knows produce must run first, so you'd get a missing-input or stale-file failure unless you add an explicit dependsOn.
  • Does the input property have to be annotated for inference to work?
    Yes. Gradle only scans properties carrying input/output annotations (@InputFile, @InputFiles, @Nested, etc.). An un-annotated field holding the provider won't be walked, so no edge is added.

Like a spreadsheet cell formula referencing another cell: you don't tell Excel the calculation order, the reference itself implies it.

saying these in an interview costs you the question

  • Saying inferred dependencies are 'the same as dependsOn, just shorter' — they also carry the value, which dependsOn does not.
  • Claiming you still must add dependsOn alongside the wiring — that defeats the purpose and is redundant.
  • Thinking plain File fields (not Property) participate in inference.

context