skip to content

When you run `gradle build`, how does Gradle decide which tasks end up in the execution graph, and how are duplicates handled?

level: middleimportance: should knowfreq 45%

answer

  1. start from requested tasks
  2. transitive closure of edges
  3. finalizers added
  4. dedup by identity, run once
  5. requested ⊆ executed; --dry-run to inspect

basics

~10 s

Gradle starts from the tasks you named (the requested tasks), walks their dependencies transitively, adds any finalizers, and deduplicates so each task appears once. The result is the ordered execution graph.

solid answer

~40 s

Graph construction begins with the **requested tasks** — the names you typed (or task selectors like `build`). For each, Gradle resolves it to actual task instances, then **transitively** pulls in everything reachable via `dependsOn` and inferred input/output edges, plus attaches `finalizedBy` finalizers. Each task is included **exactly once** no matter how many paths reach it — Gradle deduplicates by task identity. It then topologically sorts the nodes, honoring `mustRunAfter`/`shouldRunAfter` ordering and `finalizedBy` (finalizers after their finalized task). Tasks *not* reachable from any requested task are simply excluded. The distinction between *requested* (entry points) and *executed* (everything in the final graph) is important: the executed set is usually a superset of the requested set.

code

bash · 3 lines
bash
# Build and print the ordered task graph without executing actions
gradle build --dry-run
# Output lists every task in execution order, each marked SKIPPED

go deeper

for a junior

Know requested tasks pull in their dependencies and each task runs once.

for a middle

Explain transitive closure, finalizer inclusion, dedup, and requested-vs-executed.

for a senior

Use --dry-run to diagnose unexpected tasks and reason about aggregate tasks bloating the graph.

for a principal

Design task aggregation so CI graphs stay minimal and predictable across modules.

## From command line to graph ### 1. Requested tasks (entry points) What you type — `gradle build test` — becomes the set of **requested tasks**. Aggregate tasks like `build` are themselves just tasks that `dependsOn` many others. ### 2. Transitive closure For each requested task, Gradle walks **all** outgoing dependency edges: - explicit `dependsOn` - implicit edges inferred from input/output provider wiring and follows them recursively. This **transitive closure** is the set of tasks that *must* run. ### 3. Finalizers For any task that ends up scheduled, its `finalizedBy` tasks are added (and they run after). ### 4. Deduplication If `build` reaches `compileJava` through three different paths, `compileJava` still appears **once**. Gradle dedups by task identity, so each task runs at most once per build. ### 5. Ordering Gradle topologically sorts the deduplicated nodes so every dependency precedes its dependent, then applies `mustRunAfter`/`shouldRunAfter` hints and finalizer placement. A cycle is a fatal error. ## Requested vs. executed A crucial mental model: - **Requested** = entry points you asked for. - **Executed** = the full transitive set actually in the graph. The executed set is normally a **superset** of requested — e.g. `gradle test` executes `compileJava`, `processResources`, etc., none of which you named. ```bash # See the plan without running it: gradle build --dry-run # prints every task in the graph, in order, as SKIPPED ``` `--dry-run` is the simplest way to *inspect* the constructed graph: it builds the graph and prints the ordered task list without executing actions. ## Why this matters Understanding transitive inclusion explains 'why did task X run when I only asked for Y?' and helps you keep aggregate tasks from pulling in unintended work.

  • Why did compileJava run when you only asked for `gradle test`?
    test transitively depends on compileTestJava, which depends on compileJava. The executed set is the transitive closure of the requested tasks, so compileJava is pulled in even though you didn't name it.
  • How can you inspect the graph without running the build?
    Use `gradle <tasks> --dry-run`. It constructs the full graph and prints every task in execution order marked SKIPPED, so you see the plan without running any actions.

saying these in an interview costs you the question

  • Saying only the tasks you name on the command line run.
  • Claiming a task reachable by multiple paths runs once per path (it runs once).

context