An ad-hoc task prints a value every single build even when you run an unrelated task. What is going on and how do you fix it?
answer
- config phase runs every build
- work outside doLast leaks
- move into doLast/doFirst
- use Provider for lazy values
- config cache flags this
basics
~20 sThe print is outside doLast, so it runs in the configuration phase, which executes every build for all projects. Move the side-effecting code inside doLast { ... } so it only runs when the task executes.
solid answer
~50 sThis is the classic **configuration-time leak**. Any code in a `register`/`create` block but *outside* `doFirst`/`doLast` runs during the **configuration phase** — and configuration runs on essentially every invocation, for every task you might run, not just yours. So a `println` placed directly in the block fires even when you run an unrelated task. The fix is to move the work into an action: `doLast { println(...) }`, which defers it to the **execution phase** so it runs only when the task is actually executed. This isn't just cosmetic: configuration-time work slows every build and is one of the main things the **configuration cache** is designed to eliminate. With `tasks.register` (lazy/configuration-avoidance), the configuration block is at least skipped when the task isn't referenced — but anything you've put outside `doLast` will still run as soon as the task is configured for any reason. Keep real work in actions; keep the block to pure, cheap configuration.
code
kotlin · 10 lines// Before (leaks to configuration time):
tasks.register("report") {
val now = expensiveCompute() // runs every build
doLast { println(now) }
}
// After (deferred to execution):
tasks.register("report") {
doLast { println(expensiveCompute()) }
}go deeper
Recognize the print is outside doLast and move it inside.
Explain the configuration vs execution phase and why configuration runs every build across projects.
Connect to configuration avoidance vs configuration cache and to using Providers for lazy evaluation.
Drive a build-performance standard: no IO/compute at configuration time, providers for laziness, configuration cache enabled and kept green.
## The two phases A Gradle build has distinct phases: 1. **Configuration phase** — Gradle evaluates build scripts and configures the task graph for the build. This runs (broadly) every invocation, across all projects involved. 2. **Execution phase** — Gradle runs the selected tasks' **actions** in dependency order. Code you write directly inside a task-registration block is **configuration code**. Code inside `doFirst`/`doLast` is an **action** and belongs to execution. ```kotlin tasks.register("report") { println("computing") // BUG: runs at configuration, every build doLast { println("computing") // correct: runs only when 'report' executes } } ``` ## Why it 'runs on every build' Even for an unrelated target like `./gradlew compileJava`, Gradle still configures the project (and historically every project) to build the task graph, so your stray `println` executes. Expensive configuration work (network calls, file scans, heavy computation) at this point penalizes *every* build. ## The fix Move side-effecting/expensive work into `doLast` (or `doFirst`). For values you need lazily, compute them with a **`Provider`** so they're evaluated on demand rather than eagerly at configuration time. ## Two related optimizations - **Configuration avoidance** (`tasks.register` vs `tasks.create`): the *configuration block itself* is skipped unless the task is needed — but anything outside `doLast` still runs once the task is configured. - **Configuration cache**: serializes the configured task graph and reuses it across builds, skipping the configuration phase entirely on a hit. It actively flags configuration-time logic (and `project` access at execution time) as incompatible, nudging you toward keeping work in actions and using lazy providers/injected services. ## Rule of thumb The registration block should be cheap and declarative. Anything that *does* something (IO, computation, printing results) goes in `doLast`/`doFirst`.
- Does using tasks.register instead of tasks.create fix a configuration-time println outside doLast?Only partly — register skips the whole block when the task isn't needed, but once the task is configured the stray code still runs. Move it into doLast to truly fix it.
- How does the configuration cache relate to this bug?It skips the configuration phase on a cache hit and flags configuration-time work / execution-time project access as incompatible, pushing you to keep work in actions.
- How do you compute a value lazily so it isn't evaluated at configuration time?Wrap it in a Provider (e.g. provider { ... } or a Property), which is evaluated on demand rather than eagerly.
saying these in an interview costs you the question
- Thinking switching create→register alone moves the stray code into execution
- Assuming configuration code only runs for the requested task
- Doing network/file IO directly in the registration block