Why is afterEvaluate considered problematic with the configuration cache, and what should you do instead?
answer
- config cache skips configuration phase
- afterEvaluate is eager + captures state
- no Project reference at execution time
- wire Property/Provider with .set
- tasks.register lazy
basics
~20 safterEvaluate runs eager configuration logic that often captures live build state. The configuration cache stores the configured graph and skips re-running configuration, so eager hooks become fragile. Prefer lazy Provider/Property wiring that resolves at execution time.
solid answer
~50 sThe **configuration cache** serializes the result of the configuration phase so subsequent builds can skip configuration entirely. For this to work, the configured task graph must capture **lazy** references, not eager snapshots of mutable build state. `afterEvaluate` is eager: it commonly reads extension values and pokes at the project at configuration time, and code inside it often captures references (`Project`, `Task`, mutable collections) that the configuration cache forbids in task state. It also defeats the *point* of lazy configuration — you read a value at config time and bake it in, instead of wiring a `Provider` that's resolved at execution time after the user's config is applied. The recommended replacements are: model extension fields as `Property<T>`/`Provider<T>` and wire them into tasks with `.set(...)`/`map`, register tasks lazily with `tasks.register`, and use `Provider` chaining so nothing is read eagerly. When you truly must defer, scope work as narrowly as possible. This keeps builds configuration-cache compatible and faster.
code
kotlin · 10 linesabstract class GreetingExtension { abstract val message: Property<String> }
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val ext = project.extensions.create("greeting", GreetingExtension::class.java)
project.tasks.register("greet", GreetTask::class.java) {
it.message.set(ext.message) // lazy, no afterEvaluate
}
}
}go deeper
Know that reading config eagerly is discouraged and lazy Providers are preferred.
Explain that the configuration cache skips configuration and that eager hooks like afterEvaluate become fragile.
Detail which references are forbidden, why afterEvaluate captures them, and show the Property/Provider wiring replacement.
Set org-wide guidance to eliminate eager hooks in shared plugins, enforce configuration-cache compatibility in CI, and design plugin APIs around lazy properties.
## What the configuration cache does Gradle's **configuration cache** stores the fully-configured task graph after the first run. On later builds with the same inputs (tasks requested, build files, etc.), Gradle **skips the configuration phase** and loads the graph straight from the cache, then executes. This dramatically speeds up large builds. For this to be sound, everything the tasks need at execution time must be **captured safely** — either serializable inputs or **lazy providers** — never live references to mutable build-time objects like `Project`, `Gradle`, `Configuration`, or arbitrary mutable collections. ## Why afterEvaluate clashes `afterEvaluate { }` is **eager configuration-phase code**. Two problems arise: 1. **It encourages eager reads.** The usual pattern is reading an extension value inside `afterEvaluate` and assigning it somewhere. That bakes a value at config time instead of wiring a lazy provider — exactly the anti-pattern lazy configuration was designed to remove. 2. **It often captures forbidden state.** Closures inside `afterEvaluate` frequently reference `project`, tasks, or mutable collections. If those captures end up in task state, the configuration cache reports problems (it disallows referencing the `Project` at execution time). Because the configuration phase may be **skipped** when the cache hits, logic that *must* run to set things up should not live in eager hooks that the cache assumes are reproducible only from cached state. ## The preferred pattern: lazy configuration Model extension fields as `Property<T>`/`Provider<T>` and wire them straight into tasks without reading them: ```kotlin abstract class GreetingExtension { abstract val message: Property<String> } abstract class GreetTask : DefaultTask() { @get:Input abstract val message: Property<String> @TaskAction fun run() = println(message.get()) } class GreetingPlugin : Plugin<Project> { override fun apply(project: Project) { val ext = project.extensions.create("greeting", GreetingExtension::class.java) project.tasks.register("greet", GreetTask::class.java) { it.message.set(ext.message) // lazy wire — resolved at execution, after user config } } } ``` Here nothing is read eagerly: `ext.message` is a `Provider`, the user's `greeting { message = ... }` block fills it during configuration, and `message.get()` reads it at execution time. No `afterEvaluate` needed, and the task captures only a serializable `Property` — configuration-cache friendly. ## When deferral is still unavoidable Some legacy/non-lazy APIs force an eager read at config time. If you must use `afterEvaluate`, keep the block tiny, avoid capturing `Project`/tasks into task state, and prefer reading into a `Provider` you then wire lazily. ## Summary - Configuration cache = skip configuration, load configured graph. - `afterEvaluate` is eager + state-capturing → fragile under the cache. - Replace with `Property`/`Provider` wiring, `tasks.register`, and provider chaining.
- Which references are forbidden in task state under the configuration cache?Live build-time objects like Project, Gradle, Configuration, and Task — they cannot be captured for use at execution time; use Providers or serializable inputs instead.
- Does the configuration cache run afterEvaluate on a cache hit?No — on a hit the configuration phase is skipped entirely and the cached configured graph is loaded, so eager configuration hooks don't re-run.
- What's the lazy replacement for reading an extension value in afterEvaluate?Model the field as Property/Provider and wire it into the task with .set() (or map()), resolving it at execution time via get().
saying these in an interview costs you the question
- Claiming afterEvaluate is fully configuration-cache safe.
- Capturing project or tasks inside afterEvaluate and storing them in task inputs.
- Reading extension values eagerly when a Provider would defer the read cleanly.