skip to content

Task Dependencies & Ordering

How tasks are wired together: explicit dependsOn, dependencies inferred from provider outputs, ordering constraints, finalizers, and cycle detection. Interviewers probe here because a build's correctness lives in these edges.

on this pageshow

explore

questions

27

What does the dependsOn declaration do for a Gradle task, and what is its effect on the build?

level: juniorimportance: must knowfreq 80%

answer

  1. must run before + must be included
  2. builds the execution DAG
  3. name / Task / TaskProvider / closure
  4. lifecycle tasks = empty + dependsOn edges
  5. topological sort

basics

~10 s

dependsOn declares that another task must run before this one. Gradle adds the named task to the build's task graph and executes it first whenever this task is requested.

solid answer

~40 s

`dependsOn` declares an explicit ordering relationship: when task A `dependsOn` task B, requesting A guarantees B runs first (and B is included in the graph even if not asked for directly). You can pass task names as strings (`dependsOn("compileJava")`), a `Task`/`TaskProvider` reference, or a collection/closure that resolves to tasks. Gradle uses these edges to build the execution DAG, then topologically sorts it so every dependency precedes its dependent. Crucially, `dependsOn` means "must run before" AND "must be included" — it is the core mechanism for chaining work. Lifecycle tasks like `build` are nothing more than empty tasks aggregating many `dependsOn` edges (e.g. `build` depends on `assemble` and `check`).

code

kotlin · 6 lines
kotlin
tasks.register("release") {
    group = "distribution"
    dependsOn("build")            // by name
    dependsOn(tasks.named("javadoc")) // lazy TaskProvider
}
// running :release runs build (and everything build depends on) first

go deeper

for a junior

State the two effects: the dependency runs first and is included in the build. Give a simple by-name example.

for a middle

Mention the DAG / topological sort and the various accepted argument types (name, Task, TaskProvider, collection).

for a senior

Connect dependsOn to lifecycle aggregate tasks (build/check/assemble) and contrast it with output-wiring; note laziness of TaskProvider.

for a principal

Frame dependsOn as the coarse-grained edge in the build's dependency model and discuss when to prefer it vs richer wiring across a multi-module build.

## What `dependsOn` is `dependsOn` is the primary way to declare an **explicit dependency** between Gradle tasks. Saying task `A.dependsOn(B)` asserts two things: 1. **Inclusion** — if `A` is in the task graph, `B` is pulled in too, even if nobody requested `B` directly. 2. **Ordering** — `B` must complete before `A` starts. ## How it shapes the execution DAG Gradle separates the build into two phases. During **configuration**, all `dependsOn` edges are recorded. Before execution, Gradle assembles a **directed acyclic graph (DAG)** of every requested task plus everything reachable through dependency edges, then performs a **topological sort** so each task runs only after all its dependencies. With parallel execution (`--parallel` / worker API), independent branches can run concurrently, but a `dependsOn` edge is always honored. ## Ways to declare it ```kotlin tasks.register("deploy") { dependsOn("build") // by name (String) dependsOn(tasks.named("test")) // by TaskProvider — lazy, preferred dependsOn(myJarTask) // by Task/TaskProvider reference dependsOn("a", "b", "c") // multiple at once } ``` You can also pass a `Provider`, a `Callable`/closure, or a `Collection` that resolves to tasks. Passing a `TaskProvider` (from `tasks.named(...)` / `tasks.register(...)`) is preferred because it stays **lazy** and doesn't force the dependency task to be configured eagerly. ## Lifecycle / aggregate tasks Many built-in tasks carry **no action of their own** — they exist purely to group other tasks via `dependsOn`. The Java plugin's `build` task `dependsOn` `assemble` and `check`; `check` `dependsOn` `test`; `assemble` `dependsOn` `jar`. Running `./gradlew build` therefore pulls in the whole chain. You create your own aggregate tasks the same way: register an empty task and add `dependsOn` edges to the real work. ## What `dependsOn` does NOT do It does not pass data between tasks, and it does not wire task *outputs* to *inputs* (that is inferred-dependency territory). It is a coarse "run this first" edge.

  • If you request only task A and A dependsOn B, does B run?
    Yes. dependsOn pulls B into the graph and runs it before A, even though you never asked for B directly.
  • Does dependsOn pass any data or files from B to A?
    No. It only enforces ordering and inclusion. Sharing data requires wiring B's outputs to A's inputs (inferred dependencies), not dependsOn.

Like a recipe step that says 'preheat the oven before baking' — the bake step won't start until the preheat finishes, and asking to bake automatically schedules the preheat.

saying these in an interview costs you the question

  • Saying dependsOn transfers outputs/data between tasks — it only orders and includes.
  • Claiming the dependency task runs only if explicitly requested — it is pulled in automatically.

context

open as a page

What does finalizedBy do in Gradle, and when would you use it?

level: juniorimportance: must knowfreq 55%

basics

~10 s

finalizedBy declares that a finalizer task must run after a given task, even if that task fails. It's used for cleanup or teardown work, like releasing resources or generating a report after tests.

open as a page

How can you preview the ordered list of tasks Gradle will execute for a given command without actually running them?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Run the build with the --dry-run (-m) flag, e.g. gradle build --dry-run. Gradle resolves and prints the full task execution order with each task marked SKIPPED, but executes nothing.

open as a page

What is the difference between an ordering rule (mustRunAfter/shouldRunAfter) and a task dependency in Gradle?

level: juniorimportance: must knowfreq 70%

basics

~20 s

An ordering rule only fixes the relative order of two tasks IF both already end up in the task graph. It never adds a task to the graph or forces it to run, unlike a dependency, which both pulls a task in and runs it first.

open as a page

What are the different ways to specify the argument to dependsOn, and why is passing a TaskProvider preferred over a task name string?

level: middleimportance: must knowfreq 65%

basics

~20 s

You can pass a task name string, a Task or TaskProvider reference, a Provider, a Collection, or a closure. A TaskProvider is preferred because it is lazy — it doesn't force the other task to be created/configured eagerly.

open as a page

How does finalizedBy differ from dependsOn and from mustRunAfter/shouldRunAfter?

level: middleimportance: must knowfreq 50%

basics

~10 s

dependsOn pulls a task in to run before; mustRunAfter/shouldRunAfter only constrain ordering when both tasks are already in the graph. finalizedBy schedules a task to run after — even on failure — for cleanup.

open as a page

What happens when two tasks depend on each other, and how does Gradle report it?

level: middleimportance: must knowfreq 55%

basics

~10 s

Gradle cannot order a cycle, so it fails the build with a 'Circular dependency between the following tasks' error listing the tasks in the cycle. It detects this during graph resolution, before executing anything.

open as a page

When you run `gradle test`, how does Gradle decide the complete set and order of tasks it executes?

level: middleimportance: must knowfreq 70%

basics

~10 s

Gradle starts from the requested tasks, transitively pulls in all their dependencies into a directed acyclic graph (DAG), then topologically sorts that graph so every task runs after the tasks it depends on.

open as a page

How does wiring through project.layout.buildDirectory enable implicit task dependencies, and why is it preferred over hardcoded file paths?

level: middleimportance: must knowfreq 50%

basics

~10 s

layout.buildDirectory is a DirectoryProperty provider. Deriving a producer's output from it gives a lazy, relocatable location; consuming that same provider downstream lets Gradle infer the dependency instead of using a hardcoded path with dependsOn.

open as a page

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%

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.

open as a page

Explain the difference between mustRunAfter and shouldRunAfter, including how each behaves when honoring the rule would create a cycle.

level: middleimportance: must knowfreq 60%

basics

~20 s

mustRunAfter is a hard ordering constraint that Gradle always honors when both tasks run. shouldRunAfter is best-effort: Gradle tries to honor it but silently drops it if honoring it would create an ordering cycle (or conflict with a real dependency).

open as a page

How do you declare multiple dependencies on a single task, and how would you build a custom lifecycle aggregate task using dependsOn?

level: middleimportance: should knowfreq 50%

basics

~20 s

Pass several arguments to one dependsOn call, call dependsOn multiple times, or pass a collection — dependencies accumulate. A custom aggregate is an empty task that dependsOn the real tasks, grouping them under one command.

open as a page

Explain exactly when a finalizer task runs and when it doesn't, including failure and skip cases.

level: middleimportance: should knowfreq 40%

basics

~20 s

A finalizer runs after its finalized task whenever that task actually starts executing — success or failure. If the finalized task never executes (e.g. a prerequisite failed, or it isn't in the graph), the finalizer does not run.

open as a page

How can consuming a task's outputs (task.outputs.files / outputs as a FileCollection) infer a dependency without naming a specific output property?

level: middleimportance: should knowfreq 35%

basics

~10 s

A task's outputs (task.outputs.files) form a FileCollection that is built-by that task. Passing it as another task's input — e.g. inputs.files(producer) — makes Gradle infer the dependency, even without referencing a named output property.

open as a page

A teammate adds `release.mustRunAfter(build)` expecting that running `gradle release` will now also build. Why doesn't it, and what should they do instead?

level: middleimportance: should knowfreq 45%

basics

~10 s

mustRunAfter only orders tasks that are already scheduled; it never adds build to the graph. Running gradle release alone won't build. They need a real dependency (release.dependsOn(build)) or to request both tasks.

open as a page

Walk through how Gradle turns dependsOn edges into the actual execution graph, including the phases involved and how the requested tasks expand into the full plan.

level: seniorimportance: should knowfreq 45%

basics

~10 s

After configuration, Gradle takes the requested tasks, transitively follows dependsOn edges to gather every needed task, builds a directed acyclic graph, topologically sorts it, then executes — running parallel branches concurrently when enabled.

open as a page

Design a robust setup/teardown using finalizedBy for an integration-test task that starts an external resource. What pitfalls do you guard against?

level: seniorimportance: should knowfreq 33%

basics

~10 s

Have the test task dependsOn a setup task and finalizedBy a teardown task, so teardown runs even on test failure. Guard against teardown not running when setup fails, idempotent teardown, and configuration-cache compatibility.

open as a page

When you request multiple tasks like `gradle clean build`, how does Gradle build one combined graph, and what does `--exclude-task` (-x) do to it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Gradle merges all requested tasks into a single DAG, deduplicates shared dependencies (each task runs once), and topologically sorts the whole thing. -x <task> removes that task and any edge into it from the plan, so it and its now-unneeded dependencies are excluded.

open as a page

There are two different 'dependency graphs' in Gradle. What is the difference between the task graph and the dependency (artifact) resolution graph?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The task graph is the DAG of tasks Gradle will execute (e.g. compile before test). The dependency/artifact graph is the resolved tree of external modules/jars for a configuration (e.g. via dependencies). They are separate concepts solving separate problems.

open as a page

A consumer task reads a generated file but the producer doesn't run first (stale or missing file). How do you diagnose and fix a broken inferred dependency?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Usually the provider link was severed — someone called .get() while configuring, used a plain File, or left the input un-annotated. Restore lazy provider wiring (map/flatMap on the producer's output property) and the dependency re-infers; avoid patching with dependsOn.

open as a page

When wiring a consumer task's input from a producer, when do you use map versus flatMap, and how does each preserve the inferred dependency?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Use map when the transform returns a plain value, flatMap when it returns another Provider. Both keep the producer task attached, so the inferred dependency survives. Avoid .get(), which resolves eagerly and drops the producer link.

open as a page

What are the practical pitfalls of declaring ordering rules — eager vs lazy task references, accepting TaskProvider, and configuration-time cost — when wiring mustRunAfter/shouldRunAfter at scale?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Prefer passing TaskProviders or task names to mustRunAfter/shouldRunAfter and configure inside tasks.named { }, so you don't eagerly realize tasks at configuration time. Eagerly resolving with tasks.getByName(...) forces unnecessary task creation and hurts configuration performance.

open as a page

How do mustRunAfter and shouldRunAfter interact with parallel execution and determinism in a large Gradle build, and how would you use them to govern ordering across many subprojects?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Ordering rules only constrain relative order between the two named tasks; tasks with no ordering or dependency edge between them may run in parallel and in non-deterministic order. Use mustRunAfter/shouldRunAfter to force a specific sequence where it matters without making one task depend on the other.

open as a page

When is reaching for dependsOn the wrong tool, and what problems does over-relying on explicit dependsOn cause in a build?

level: seniorimportance: nice to knowfreq 35%

basics

~10 s

dependsOn 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.

open as a page

Can finalizedBy create a circular dependency, and how does the finalization direction interact with the task graph?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

Finalization is one-directional: requesting a finalizer never pulls in its finalized task. But combining finalizedBy with dependsOn carelessly can create a true cycle, which Gradle rejects with a circular-dependency error.

open as a page

How can build logic make decisions based on whether a particular task will run, and why must this be done at the right phase?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

Use gradle.taskGraph.whenReady { graph -> } (or graph.hasTask(...)). It fires after the full task graph is resolved but before execution, so logic can branch on whether, say, a release task is in the plan.

open as a page

Do inferred provider dependencies work across projects in a multi-module build, and how should they be exposed between modules?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Yes — a provider from another project's task carries that task as producer, so consuming it infers a cross-project dependency. The clean way to expose outputs across modules is via variant-aware configurations (consumable/resolvable) rather than reaching into another project's tasks.

open as a page