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.
answer
- init / configure / execute
- edges declared at configuration, no run
- entry tasks -> transitive expansion -> DAG
- topological sort then execute
- taskGraph.whenReady to inspect
basics
~10 sAfter 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.
solid answer
~40 sGradle runs in three phases: **initialization**, **configuration**, and **execution**. `dependsOn` edges are declared during configuration but no task runs yet. Between configuration and execution Gradle constructs the **task execution graph**: starting from the requested (entry) tasks, it transitively walks `dependsOn` edges to collect every reachable task into a DAG, then performs a **topological sort** so each task is scheduled only after all its dependencies. The graph is finalized — you can inspect it via `gradle.taskGraph.whenReady { }` — before any action runs. During execution Gradle walks the sorted plan; with `--parallel` and project isolation, independent branches execute on multiple workers, but every `dependsOn` edge is still honored. Up-to-date checks and the build cache may skip a task's work, yet the task still occupies its node in the graph.
code
kotlin · 4 linesgradle.taskGraph.whenReady {
val tasks = it.allTasks // fully resolved, topologically sorted
println("Execution plan: " + tasks.joinToString(" -> ") { t -> t.path })
}go deeper
Know there is a configuration phase then an execution phase, and dependencies run first.
Describe the entry-task expansion into a DAG and the topological sort.
Explain whenReady inspection, parallel branch execution, and that caching/up-to-date doesn't remove graph nodes.
Use taskGraph hooks for governance (e.g. gating deploy tasks) and reason about plan determinism and parallel build performance at scale.
## The three build phases 1. **Initialization** — Gradle decides which projects participate (settings, included builds). 2. **Configuration** — every build script runs; tasks are registered and their `dependsOn`/ordering relationships declared. With configuration avoidance, only tasks that may be needed are realized. **Nothing executes here.** 3. **Execution** — tasks actually run. ## Building the task execution graph Between configuration and execution, Gradle computes the **execution plan**: - The **entry points** are the tasks you requested (CLI args, default tasks, or `gradle.startParameter`). - Gradle **transitively expands** each entry task by following its `dependsOn` set, then those tasks' `dependsOn`, and so on, collecting a closure of tasks. - The result is a **directed acyclic graph** whose nodes are tasks and edges are dependencies. (A cycle is an error — but that is a separate concern from declaring edges.) - Gradle runs a **topological sort**: a linear-ish ordering where every task appears after all tasks it depends on. Where ordering is unconstrained, Gradle is free to choose. You can observe the finalized graph: ```kotlin gradle.taskGraph.whenReady { graph -> println("Will run ${graph.allTasks.size} tasks") if (graph.hasTask(":deployProd")) { /* require approval */ } } ``` ## Execution and parallelism During execution Gradle walks the plan. With `org.gradle.parallel=true`, tasks in **independent branches** (no path of dependsOn between them) may run **concurrently** on separate workers; a `dependsOn` edge forces serialization across it. Independent of parallelism, **incremental build** (up-to-date checks) and the **build cache** may make a task's action a no-op — but the node still exists in the graph and its dependents still wait for it to resolve. ## De-duplication and shared dependencies Because the graph is a **set** of nodes, a task reached through multiple paths is included **once** and runs at most once. This is why aggregate tasks that share transitive dependencies don't cause duplicate work. ## Mental model Think of `dependsOn` as adding directed edges; Gradle's job before execution is to compute the reachable subgraph from your entry points and linearize it. Everything else — caching, parallelism — operates on top of that fixed plan.
- At which point is the task graph fully known, and how can you hook into it?After configuration completes and before execution starts. gradle.taskGraph.whenReady { } fires once the graph is finalized, letting you inspect allTasks or gate on whether a specific task is present.
- If a task is up-to-date or pulled from the cache, is it removed from the graph?No. The node remains in the execution plan; its action is simply skipped/served from cache. Dependents still treat it as a satisfied predecessor.
- How does --parallel interact with dependsOn?Parallel execution can run independent branches concurrently, but any dependsOn edge is always respected, serializing the dependent after its dependency.
Configuration writes the to-do list with arrows showing what must precede what; before starting, Gradle traces every arrow back from your goals to make one ordered plan, then works through it (with helpers on parallel branches).
saying these in an interview costs you the question
- Saying tasks execute during the configuration phase — only graph wiring happens there.
- Claiming a task reached via two paths runs twice — the graph de-duplicates nodes.