What is Gradle's Configuration Avoidance API, and what problem does it solve?
answer
- config vs execution phase
- TaskProvider, no realize
- register/named/configureEach
- avoid create/getByName/all
- skip unused tasks
basics
~10 sIt's a set of lazy task APIs (TaskProvider, tasks.named, configureEach) that let Gradle skip creating and configuring tasks you don't run, cutting configuration-time cost.
solid answer
~30 sGradle has two phases: configuration (it builds the task graph) and execution (it runs requested tasks). Historically every task was eagerly instantiated and configured on every build, even tasks you never ran. The Configuration Avoidance API defers that work: `tasks.register(...)` returns a `TaskProvider<T>` instead of the task, and the task object is only created and configured ("realized") if something actually needs it for the requested build. APIs like `tasks.named(...)`, `configureEach`, and `withType().configureEach` keep configuration lazy end-to-end. The payoff is faster configuration time, especially in large multi-project builds where most tasks are irrelevant to any given invocation.
code
kotlin · 10 linesval generate = tasks.register<Copy>("generate") {
from("src/templates")
into(layout.buildDirectory.dir("generated"))
}
// stays lazy: 'build' is only configured if realized
tasks.named("build") { dependsOn(generate) }
// applies to every Test task, lazily as each is realized
tasks.withType<Test>().configureEach { maxParallelForks = 4 }go deeper
Name the two APIs you'd reach for (register, configureEach) and that they skip work for unused tasks.
Explain the configuration-vs-execution phases and that TaskProvider defers realization.
Discuss the eager APIs that defeat avoidance and why keeping the whole chain lazy matters in large builds.
Frame it as a configuration-time budget across a monorepo and how to enforce lazy patterns via conventions/lint.
## The two-phase model Every Gradle build runs in phases. During the **configuration phase**, Gradle evaluates build scripts and builds the task graph — it decides what tasks exist and how they depend on each other. During the **execution phase**, it actually runs the subset of tasks needed for the requested goals. The cost matters: configuration runs on *every* invocation, even `./gradlew help`. In a 200-module build, eagerly creating and configuring thousands of task objects you'll never run is pure waste. ## What "realization" means "Realizing" a task = instantiating the task object and running its configuration closures. The Configuration Avoidance API exists to **defer or skip** realization for tasks not needed by the current build. ## The lazy primitives - `tasks.register("name", Type) { ... }` returns a **`TaskProvider<T>`**. The configuration block and the task object are only realized if the provider is queried (e.g. the task is requested, or another realized task depends on it). - `tasks.named("name")` returns a `TaskProvider` **without realizing** the task — you can attach more lazy configuration to it. - `tasks.named("name") { ... }` adds configuration that runs only if/when the task is realized. - `tasks.configureEach { ... }` registers configuration applied lazily to *every* task — current and future — but only as each task is actually realized. - `tasks.withType(Test::class).configureEach { ... }` does the same, scoped to a type. ## The eager APIs that defeat it These force immediate realization of matching tasks and should be avoided in hot paths: - `tasks.create(...)` — realizes immediately. - `tasks.getByName(...)` — realizes that task now. - `tasks.all { ... }` and `tasks.withType(T) { ... }` (the closure overload) — realize *every* matching task immediately. - Iterating `tasks` or calling `.getByName` inside a `configureEach` can cascade realization. ## Rule of thumb Prefer `register` over `create`, `named` over `getByName`, and `configureEach` over `all`. Keep the whole chain returning providers so nothing realizes until the requested build truly needs it. ```kotlin // Lazy end-to-end val generate = tasks.register<Copy>("generate") { from("src/templates"); into(layout.buildDirectory.dir("generated")) } tasks.named("build") { dependsOn(generate) } tasks.withType<Test>().configureEach { maxParallelForks = 4 } ```
- Does using register guarantee the task is never created?No — register only *defers* creation. If the task is requested on the command line, or a realized task depends on it, or some eager API queries it, it still gets realized. Avoidance only helps for tasks nothing needs in that invocation.
- Which build phase does this optimize, configuration or execution?Configuration. It reduces the cost of building the task graph each run; it doesn't change execution-phase work for tasks you actually run.
Like lazy-loading a web page's images: the browser only fetches the picture you actually scroll to, not every image on every page you might visit.
saying these in an interview costs you the question
- Claiming it speeds up execution of tasks you run — it targets configuration time.
- Saying registered tasks are never created even when requested.