Why is doing work in doFirst/doLast (instead of the task configuration block) important for the configuration cache and incremental builds?
answer
- config cache serializes the task graph
- config phase must be pure
- side effects -> actions only
- no Project in action closures
- config-block code skipped on cache hit
basics
~20 sThe 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.
solid answer
~50 sGradle's **configuration cache** serializes the fully configured task graph after the configuration phase and reuses it on later builds, skipping configuration entirely when inputs are unchanged. That only works if the configuration phase is **pure** — it just wires up tasks and never does real work. So any side effect (reading/writing files, running tools, querying the environment at the wrong time) must live in an **action** (`doFirst`, `doLast`, or `@TaskAction`), which still runs every time the task executes, even on a config-cache hit. Code in the configuration block runs only when the cache *misses*; on a hit it's skipped, so side effects there silently disappear. Worse, touching disallowed state (e.g. reading `System.getenv` at configuration time, or capturing a `Project` reference in an action) makes the build **incompatible with the configuration cache** and Gradle reports it. The discipline — wiring in config, work in actions — is also what makes incremental and up-to-date checks behave correctly.
code
kotlin · 8 lines// Config-cache friendly: defer reads into the action
tasks.register("printVersion") {
val versionFile = layout.projectDirectory.file("VERSION")
inputs.file(versionFile)
doLast {
println("version = " + versionFile.asFile.readText().trim())
}
}go deeper
Know that real work goes in doLast and not the config block; the cache angle is beyond junior.
Explain that the config block runs at configuration time and may be skipped, so side effects must be in actions.
Explain the configuration cache lifecycle, why config must be pure, and how to defer reads via Providers/inputs to stay compatible.
Drive adoption of the configuration cache org-wide: codify 'no work at configuration time', audit plugins for violations, and treat compatibility as a CI gate.
## What the configuration cache does The **configuration cache** is a Gradle feature (`org.gradle.configuration-cache=true`) that: 1. Runs the configuration phase once, then **serializes** the resulting task graph (tasks + their resolved inputs/outputs) to disk. 2. On subsequent runs with the same requested tasks and unchanged build inputs, it **skips initialization and configuration** entirely and deserializes the cached graph straight into the execution phase. This can hugely speed up large builds, but it imposes rules on *where* code may run. ## The pure-configuration requirement For caching to be correct, the configuration phase must be **side-effect free** and must not capture forbidden objects. Consequences: - **Side effects belong in actions.** Reading or writing files, invoking external processes, computing results to print — put these in `doFirst`/`doLast`/`@TaskAction`. On a config-cache **hit**, the configuration block does not run, so anything there is simply skipped. Actions always run when the task executes. - **No live `Project` at execution time.** An action closure must not reference the `Project` object or arbitrary mutable build state; doing so is a config-cache violation. Capture only serializable values (or use `Provider`/`Property`, `ValueSource`, services) at configuration time and read them in the action. - **No environment reads at configuration time** unless declared (e.g. via `providers.environmentVariable(...)`), or the cache key won't track them. ### Example of the trap ```kotlin // BAD: work + project access at configuration time, lost on cache hit tasks.register("bad") { val content = file("VERSION").readText() // runs at config time only doLast { println(content) } // captured value may be stale / cache-incompatible } // GOOD: defer the read into the action via a Provider tasks.register("good") { val versionFile = layout.projectDirectory.file("VERSION") inputs.file(versionFile) doLast { println(versionFile.asFile.readText()) } } ``` ## Tie-in with incremental builds The same separation underpins **up-to-date checks**: Gradle compares declared `inputs`/`outputs` (wired at configuration time) and runs the **actions** only when they changed. If work happens at configuration time, it runs unconditionally and isn't tracked by incremental build at all. ## Rule of thumb Configuration = describe *what* the task is and *how* it's wired (inputs, outputs, dependencies, Providers). Actions (`doFirst`/`doLast`/`@TaskAction`) = do the *work*, lazily, only when the task runs.
- Why can't a doLast closure reference the Project object when the configuration cache is on?The Project isn't serializable into the cached graph and represents live mutable state; capturing it is a config-cache violation. Capture serializable values or Providers instead.
- On a configuration-cache HIT, does code in the task's register{} block run?No. The configuration phase is skipped and the cached graph is used directly, so only the task's actions run.
- How do you safely read an environment variable for use in an action?Use providers.environmentVariable("X") at configuration time to get a Provider, then resolve it inside the action; this keeps the value tracked by the cache key.
Configuration cache is like saving a fully wired stage set so you skip rebuilding it each night; the actors (actions) still perform every show, but you must never bake the live performance into the set blueprint.
saying these in an interview costs you the question
- Saying the configuration cache reruns the configuration block on every build.
- Claiming side effects in the configuration block are fine because 'it works locally'.
- Referencing project or file(...) inside a doLast under config cache without deferring.