Explain Gradle's configuration phase versus execution phase. What code in a build script runs in each?
answer
- init → config → execution
- whole script body = configuration
- doLast/@TaskAction = execution
- config runs every build
- laziness enables config cache
basics
~20 sConfiguration 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 sGradle 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 linestasks.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
Name the three phases and that doLast runs at execution, script body at configuration.
Demonstrate with the A/B/C print example and explain why config runs every build.
Tie laziness (register/named, Provider/Property) to configuration cost and the configuration cache prerequisite.
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