skip to content

doFirst & doLast Action Lists

Appending and prepending work with doLast and doFirst, and the ordered action list that results. Interviewers ask it to test the configuration-block versus action-block timing distinction.

on this pageshow

questions

5

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

open as a page

How do doFirst{} and doLast{} compose into a task's action list, and what is the execution order if you call them multiple times?

level: middleimportance: must knowfreq 65%

basics

~20 s

A task holds an ordered list of actions. doLast appends to the end; doFirst prepends to the front. Multiple doFirst calls stack so the last-added doFirst runs first; doLast actions run in the order added.

open as a page

What is a lifecycle (aggregate) task with no actions, and how does it differ from a task that has doLast actions?

level: middleimportance: should knowfreq 45%

basics

~20 s

A lifecycle task has an empty action list — it does no work itself and exists only to group/depend on other tasks (like build or check). A task with doLast actions actually performs work when executed.

open as a page

Why is doing work in doFirst/doLast (instead of the task configuration block) important for the configuration cache and incremental builds?

level: seniorimportance: should knowfreq 50%

basics

~20 s

The configuration cache stores the configured task graph and reuses it, skipping the configuration phase. Side effects must live in actions (doFirst/doLast/@TaskAction) so they still run on cache hits; configuration-time side effects would be lost or break the cache.

open as a page

What was the `<<` (leftShift) operator on tasks, why was it removed, and what replaces it?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

In old Groovy DSL, task foo << { ... } was shorthand for adding a doLast action. It was deprecated in Gradle 3.x and removed in Gradle 5.0 because it blurred configuration vs action code. Use doLast { } instead.

open as a page