When is reaching for dependsOn the wrong tool, and what problems does over-relying on explicit dependsOn cause in a build?
answer
- dependsOn = ordering only, no data
- inputs/outputs drive caching, not edges
- over-use breaks incrementality
- wire input to output Provider instead
- keep dependsOn for ordering + aggregates
basics
~10 sdependsOn only orders and includes tasks; it doesn't wire outputs to inputs. Over-using it for data flow leads to missing dependencies, broken incremental builds and cache misses, and a brittle hand-maintained graph.
solid answer
~40 s`dependsOn` is a **coarse** edge: "run B before A." It does not connect B's *outputs* to A's *inputs*, so it carries no up-to-date or caching information about the data flowing between tasks. Relying on it to model data flow is an anti-pattern: if A actually consumes a file B produces, declaring only `dependsOn` means Gradle can't detect when that file changes for A's incrementality, and a custom task may even break the build cache. The idiomatic alternative is to wire A's input property to B's output `Provider`, which makes the dependency **implicit and automatic** while also feeding up-to-date checks. Over-using `dependsOn` also produces a sprawling, manually-maintained graph that's easy to get wrong (forgotten edges, accidental over-serialization that kills parallelism). Reserve explicit `dependsOn` for pure ordering with no data exchange and for assembling lifecycle/aggregate tasks.
code
kotlin · 7 lines// Anti-pattern: ordering only, no input tracking
tasks.named("consume") { dependsOn("generate") }
// Better: wiring implies the dependency AND tracks the file as input
tasks.named<ConsumeTask>("consume") {
inputFile.set(generateTask.flatMap { it.outputFile })
}go deeper
Know that dependsOn only orders tasks and does not move files between them.
Explain that caching/incrementality use inputs/outputs, so data flow should be wired, not expressed via dependsOn.
Contrast explicit edges with provider wiring and call out the incrementality, cache-correctness, and parallelism implications.
Set conventions: wire input↔output for data flow, reserve dependsOn for ordering/aggregation, and review builds for over-serialization across the module graph.
## The core limitation `dependsOn` answers one question: *what must run before this task?* It says nothing about *what data* moves between tasks. That distinction matters because Gradle's **incremental build** and **build cache** track a task's declared **inputs** and **outputs**, not its `dependsOn` edges. ## Why over-use causes problems 1. **Broken incrementality / cache** — if task A reads a file produced by task B but you only declared `dependsOn(B)`, Gradle doesn't know that file is an input to A. A may be considered up-to-date when B's output changed, or its cache key may be wrong. The fix is to make the produced file a **declared input** of A. 2. **Fragile, hand-maintained graph** — large builds wired entirely with explicit edges accumulate forgotten or stale dependencies. Refactor a producer and every manual `dependsOn` referencing it must be updated by hand. 3. **Lost parallelism** — sprinkling `dependsOn` to "be safe" can over-serialize independent work, hurting `--parallel` performance. ## The idiomatic alternative: wire outputs to inputs Instead of an explicit edge, connect a consumer's input property to the producer's output `Provider`. Gradle then **infers** the ordering automatically *and* gains the input/output knowledge for caching: ```kotlin val generate = tasks.register<GenerateTask>("generate") { outputFile.set(layout.buildDirectory.file("gen/data.json")) } tasks.register<ConsumeTask>("consume") { // wiring the Provider implies the dependency — no explicit dependsOn inputFile.set(generate.flatMap { it.outputFile }) } ``` Because `consume`'s input points at `generate`'s output provider, Gradle runs `generate` first **and** treats its file as a tracked input of `consume`. ## When dependsOn IS right - **Pure ordering** with no data exchange (e.g. "clean caches before a smoke task"). - **Lifecycle/aggregate** tasks that group work (`build`, `check`, your `qualityGate`). - Depending on a **third-party task** you can't easily wire by provider. ## Rule of thumb If one task *consumes what another produces*, wire input↔output. If you merely need *ordering or grouping*, `dependsOn` is correct.
- Why can dependsOn alone break a task's up-to-date check?Up-to-date checks compare declared inputs/outputs. A dependsOn edge isn't an input, so a file produced by the dependency isn't tracked; the consumer may be wrongly considered up-to-date when that file changes.
- Give a legitimate use of explicit dependsOn.Pure ordering with no data flow (run a cleanup before a task) and building aggregate/lifecycle tasks like a custom qualityGate that groups test, detekt, and lint.
dependsOn is telling two workers the order to work in; wiring outputs to inputs is handing the first worker's finished part directly to the second — so the second automatically redoes its job when the part changes.
saying these in an interview costs you the question
- Recommending dependsOn to make a consumer pick up a producer's file — that doesn't track the file as an input.
- Adding dependsOn edges 'to be safe' without realizing it can over-serialize and harm parallel builds.