skip to content

In a Gradle task definition, what is the difference between code in the configuration block and code inside doFirst{}/doLast{}, and when does each run?

level: juniorimportance: must knowfreq 80%

answer

  1. config phase vs execution phase
  2. register body = configuration
  3. doLast closure = execution-only
  4. actions skip when UP-TO-DATE
  5. work in actions, wiring in config

basics

~10 s

Configuration-block code runs during the configuration phase for every build invocation. doFirst{}/doLast{} are action closures that run only in the execution phase, and only if the task actually executes.

solid answer

~40 s

Gradle runs in phases: **initialization**, **configuration**, then **execution**. Code written directly in a `tasks.register("x") { ... }` block (the configuration block) runs during the *configuration* phase — every build, regardless of whether the task ends up in the task graph or runs. Code inside `doFirst { }` and `doLast { }` is added to the task's *action list* and runs during the *execution* phase, only if the task is actually selected and not up-to-date/skipped. So setting properties, wiring inputs/outputs, and dependencies belongs in the configuration block; the actual work (file copying, running a tool, printing results) belongs in `doLast`. Putting work in the configuration block makes it run on *every* invocation — a classic performance bug and the core idea behind configuration avoidance.

code

kotlin · 9 lines
kotlin
tasks.register("report") {
    group = "verification"            // configuration phase (every build)
    val outFile = layout.buildDirectory.file("report.txt")
    outputs.file(outFile)
    doLast {                          // execution phase (only if run)
        outFile.get().asFile.writeText("done")
        println("wrote report")
    }
}

go deeper

for a junior

Know the basic split: register-block code configures (always runs), doLast does the actual work at execution time.

for a middle

Articulate the three phases and that actions are skipped for up-to-date/skipped tasks; tie to where to place side effects.

for a senior

Connect to configuration avoidance and configuration-time performance; explain why side effects in config break incremental builds and config cache.

for a principal

Frame as a team convention: enforce 'no side effects at configuration time' to keep configuration cache compatible and builds fast at scale.

## The three build phases Every Gradle build runs in three ordered phases: 1. **Initialization** — Gradle decides which projects participate (settings.gradle). 2. **Configuration** — Gradle evaluates build scripts and *configures* every task object it needs to know about. The configuration block of a task runs here. 3. **Execution** — Gradle works out the requested task graph and runs the **actions** of each selected task, in order. ## Configuration block vs action block When you write: ```kotlin tasks.register("greet") { // <-- configuration block group = "demo" description = "says hi" doLast { // <-- action block (execution phase) println("hello") } } ``` The body of the `register` lambda (`group =`, `description =`, the *call* to `doLast`) executes during the **configuration phase**. The closure *passed to* `doLast` is stored in the task's action list and only invoked during the **execution phase**, and only if `greet` is actually run. ## Why it matters - Anything expensive or with side effects (reading files, calling external services, `println` of results) put **inside an action** (`doLast`), never in the configuration block. Configuration-block code runs on *every* build — even `./gradlew help` — which slows down every invocation. - `doFirst`/`doLast` only fire when the task executes. If the task is `UP-TO-DATE`, `SKIPPED`, or `NO-SOURCE`, its actions do **not** run. - This distinction is the foundation of **configuration avoidance**: `tasks.register` (lazy) configures a task only when it's needed, versus `tasks.create` (eager) which configures immediately. ## The action list A task is really an ordered list of `Action<Task>` objects. `doFirst` prepends, `doLast` appends. A task with no actions (a lifecycle task like `build`) simply has an empty action list and does nothing itself.

  • Why is putting println("building") directly in the register block usually a mistake?
    It runs during the configuration phase on every single invocation (even unrelated tasks like `help`), not when the task executes. Side effects belong in a doLast action.
  • If a task is UP-TO-DATE, do its doLast actions run?
    No. When a task is up-to-date (or skipped/no-source), its action list is not executed at all.

The configuration block is writing the recipe card (always done when you open the cookbook); doFirst/doLast are actually cooking — only happens when you decide to make that dish.

saying these in an interview costs you the question

  • Saying doLast runs during the configuration phase.
  • Claiming the whole register{} body runs only when the task executes.
  • Putting expensive work or println of results in the configuration block.

context