skip to content

Why is afterEvaluate considered problematic with the configuration cache, and what should you do instead?

level: seniorimportance: should knowfreq 38%

answer

  1. config cache skips configuration phase
  2. afterEvaluate is eager + captures state
  3. no Project reference at execution time
  4. wire Property/Provider with .set
  5. tasks.register lazy

basics

~20 s

afterEvaluate 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 s

The **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 lines
kotlin
abstract 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

for a junior

Know that reading config eagerly is discouraged and lazy Providers are preferred.

for a middle

Explain that the configuration cache skips configuration and that eager hooks like afterEvaluate become fragile.

for a senior

Detail which references are forbidden, why afterEvaluate captures them, and show the Property/Provider wiring replacement.

for a principal

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.

context