What does it mean for a task dependency to be "inferred" in Gradle, and how does it differ from calling dependsOn?
answer
- value + ordering travel together
- Property set from producer's Provider
- annotated input → Gradle scans backing provider
- no dependsOn string
- .get() severs the link
basics
~10 sIf 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 sAn 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 linesabstract 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
Know that wiring one task's output provider into another's input makes Gradle run them in order automatically, without dependsOn.
Explain the mechanics: annotated input property holding a producer-backed provider; Gradle scans inputs and adds the edge; .get() breaks it.
Discuss why coupling value and ordering eliminates a whole bug class, and when you still legitimately need dependsOn (pure lifecycle tasks).
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.