skip to content

Explain Gradle's configuration phase versus execution phase. What code in a build script runs in each?

level: middleimportance: must knowfreq 75%

answer

  1. init → config → execution
  2. whole script body = configuration
  3. doLast/@TaskAction = execution
  4. config runs every build
  5. laziness enables config cache

basics

~20 s

Configuration phase runs the build scripts top-to-bottom to build the task graph. Execution phase runs the doFirst/doLast/@TaskAction bodies of the selected tasks. Code in the script body runs at configuration; code inside task actions runs at execution.

solid answer

~50 s

Gradle runs in three phases: **initialization** (settings.gradle, which projects participate), **configuration** (every build script is evaluated top-to-bottom, building the task object graph), and **execution** (the action bodies of the requested tasks and their dependencies run). Crucially, the **whole** build script body and every `tasks.register`/`task {}` configuration block run during configuration — *every build, even if the task won't execute*. Only `@TaskAction`, `doFirst {}`, and `doLast {}` bodies run during execution, and only for tasks actually in the executed graph. This is why putting expensive work (file I/O, network calls, `exec`) directly in the script body or in a task's configuration closure is a performance anti-pattern — it runs eagerly on every invocation. The fix is lazy registration (`tasks.register`) and the Provider/Property API so values are computed only when needed, which also enables the configuration cache.

code

kotlin · 9 lines
kotlin
tasks.register("report") {
    // configuration phase: runs on every build
    val outFile = layout.buildDirectory.file("report.txt")
    outputs.file(outFile)
    doLast {
        // execution phase: only when 'report' runs
        outFile.get().asFile.writeText("generated")
    }
}

go deeper

for a junior

Name the three phases and that doLast runs at execution, script body at configuration.

for a middle

Demonstrate with the A/B/C print example and explain why config runs every build.

for a senior

Tie laziness (register/named, Provider/Property) to configuration cost and the configuration cache prerequisite.

for a principal

Reason about build-time SLAs across a large monorepo: enforcing configuration-cache compatibility, avoiding cross-project config-time coupling, measuring with --profile/build scans.

## The three phases 1. **Initialization** — Gradle evaluates `settings.gradle(.kts)` to determine which projects make up the build and creates a `Project` instance for each. 2. **Configuration** — Gradle evaluates **every** participating project's build script from top to bottom. The result is the fully-built **task graph** (a DAG of `Task` objects with dependencies). Everything in the script *body* runs here. 3. **Execution** — Gradle determines the subset of tasks needed for the requested task(s) (e.g. `./gradlew build`), orders them, and runs their **actions** (`@TaskAction` / `doFirst` / `doLast`). ## What runs where ```kotlin println("A: configuration") // runs every build, configuration phase tasks.register("hello") { println("B: configuration") // task CONFIGURATION — still configuration phase doLast { println("C: execution") // only when 'hello' is actually executed } } ``` Running `./gradlew help` prints A and B but **not** C — the task was configured but never executed. Running `./gradlew hello` prints A, B, then C. ## Why this matters: performance Because configuration runs on **every** invocation for **every** task (unless you use task-configuration avoidance), expensive work placed at the wrong altitude slows down *all* builds. Anti-patterns: - Reading files, querying a DB, or running `project.exec {}` in the script body. - Using eager `tasks.create(...)` or accessing `tasks.getByName(...)` which forces configuration of tasks you may not run. ## The fix: laziness - **Task-configuration avoidance**: `tasks.register("x") { ... }` defers the configuration closure until the task is actually needed; `tasks.named("y") { ... }` configures only if already realized. Compare to eager `tasks.create` / `tasks.getByName`. - **Provider/Property API**: wrap values in `Provider<T>` / `Property<T>` so they're computed lazily at execution time. Move side-effecting work into `doLast {}` or a custom task's `@TaskAction`. This discipline is also a prerequisite for the **configuration cache**, which serializes the configured task graph so the configuration phase can be skipped entirely on subsequent builds.

  • How can you skip the configuration phase entirely on repeat builds?
    Enable the configuration cache (`org.gradle.configuration-cache=true`). Gradle serializes the configured task graph and reuses it when inputs to configuration haven't changed.
  • What's the difference between tasks.create and tasks.register for configuration cost?
    `create` eagerly instantiates and configures the task immediately; `register` is lazy — the configuration closure runs only if the task is realized (needed). Prefer `register` to avoid configuring unused tasks.
  • Where should a network call needed by a task go?
    Inside the task's @TaskAction / doLast (execution), never in the script body or configuration closure, so it only runs when the task actually executes.

Configuration is drawing the blueprint of every room in the house; execution is walking into only the rooms you actually need and turning on their lights.

saying these in an interview costs you the question

  • Saying the script body runs during execution
  • Claiming doLast {} runs at configuration time
  • Believing task configuration closures only run when the task executes

context