skip to content

What happens during Gradle's Execution phase, and how does it differ from the Configuration phase?

level: juniorimportance: must knowfreq 70%

answer

  1. third / last phase
  2. runs @TaskAction + doFirst/doLast
  3. config builds objects, exec runs work
  4. outcome labels UP-TO-DATE / SKIPPED
  5. command-line tasks + their deps

basics

~20 s

Execution is the third (last) phase. Gradle runs the @TaskAction methods of the selected tasks in dependency order. Configuration, the earlier phase, only builds task objects and wires their dependencies — it does no actual work.

solid answer

~30 s

A Gradle build runs in three phases: **Initialization** (decide which projects participate), **Configuration** (evaluate every build script, create task objects, wire `dependsOn`/inputs/outputs), and **Execution**. In Execution, Gradle takes the tasks you requested on the command line, builds and topologically orders the task graph, and then runs each selected task's `@TaskAction` methods. Crucially, only the *actions* run here — all the DSL configuration code (`tasks.register { ... }` blocks, `doFirst`/`doLast` registration) ran earlier in Configuration. For each task Gradle reports an outcome: executed, `UP-TO-DATE`, `FROM-CACHE`, `SKIPPED`, or `NO-SOURCE`. If a task action throws, the build fails fast (unless `--continue`).

code

kotlin · 8 lines
kotlin
tasks.register("greet") {
    // CONFIGURATION time: runs on every build, even if 'greet' isn't requested
    println("configuring greet")
    doLast {
        // EXECUTION time: runs only when 'greet' actually executes
        println("Hello from execution!")
    }
}

go deeper

for a junior

Name the three phases and state that Execution runs the actions of the requested tasks in dependency order.

for a middle

Distinguish configuration-time vs execution-time code, and list the outcome labels (UP-TO-DATE, SKIPPED, FROM-CACHE).

for a senior

Explain how task selection + graph ordering feeds Execution, and why misplacing code in configuration blocks hurts every build.

for a principal

Relate phase separation to configuration-cache and parallel-execution guarantees, and to enforcing fast configuration across a large multi-project build.

## The three lifecycle phases Every Gradle invocation moves through three phases, strictly in order: 1. **Initialization** — Gradle evaluates `settings.gradle(.kts)` and decides which projects make up the build, creating a `Project` instance for each. 2. **Configuration** — Gradle executes the body of *every* build script (subject to configuration avoidance). This is where task **objects** are created and their wiring is declared: `dependsOn`, `inputs`, `outputs`, `mustRunAfter`. No task *work* happens here. 3. **Execution** — Gradle determines which of the configured tasks to actually run (from the command-line request + their dependencies), orders them, and invokes their actions. ## What runs in Execution A task is a sequence of **actions**. Actions come from: - The `@TaskAction`-annotated method(s) of a typed task class. - `doFirst { }` and `doLast { }` blocks (prepended/appended action lists). During Execution, for each task in graph order, Gradle: 1. Computes the task's **input/output snapshot** and checks up-to-date / build-cache state. 2. If work is needed, runs `doFirst` actions, then `@TaskAction`, then `doLast` actions. 3. Records an **outcome**. ``` Configuration time: tasks.register("hello") { doLast { println("hi") } } // runs in Configuration; the doLast LAMBDA is only REGISTERED Execution time: the registered doLast lambda actually prints "hi" ``` ## Common confusion: code placement Code written directly in a `register`/`configure` block runs at **Configuration** time, on *every* build, even if the task is never executed. Code inside `doLast { }`/`doFirst { }`/`@TaskAction` runs at **Execution** time, only when the task actually runs. Putting expensive logic in the configuration block is a classic mistake that slows every invocation. ## Outcomes you will see - *(no label)* — the action ran. - `UP-TO-DATE` — inputs/outputs unchanged, skipped. - `FROM-CACHE` — outputs pulled from the build cache. - `SKIPPED` — an `onlyIf {}` predicate returned false, or `--exclude-task`. - `NO-SOURCE` — the task had declared sources but they were empty.

  • If you run `gradle help`, does the `doLast` block of a `greet` task you defined still run?
    No. Its configuration block may run (unless avoided via lazy registration), but the `doLast` action only runs if `greet` itself is in the executed task graph.
  • Where should expensive computation live to avoid slowing every build?
    Inside `@TaskAction`/`doLast` (execution time), not in the task's configuration block, so it only runs when the task actually executes.

Configuration is reading the whole recipe and laying out ingredients; Execution is actually cooking the dishes you ordered, in the order the recipe requires.

saying these in an interview costs you the question

  • Saying task actions run during Configuration.
  • Claiming code in a register{} block runs only when the task executes — it runs at configuration time.

context