Compare lazy and eager task handling (tasks.register/named vs tasks.create/getByName). When does eagerness hurt and why?
answer
- register/named = lazy
- create/getByName/all = eager
- realization runs config closure
- config runs every build
- configureEach not all
- prerequisite for config cache
basics
~10 stasks.register and tasks.named are lazy — the task is created/configured only if needed. tasks.create and tasks.getByName are eager — they realize and configure the task immediately, even if it's never run, increasing configuration time.
solid answer
~50 sGradle's **task-configuration avoidance** API makes task handling lazy. `tasks.register("x") { ... }` returns a `TaskProvider` and defers both creation and the configuration closure until the task is actually needed (in the executed graph or referenced by a realized task). `tasks.named("y") { ... }` configures an existing registration lazily. By contrast, the legacy eager API — `tasks.create("x") { ... }`, `tasks.getByName("y")`, `tasks.withType(...).all { }` (which realizes matches) — **forces immediate realization and configuration** of those tasks on every build, regardless of whether they'll run. In a large multi-module build with hundreds of tasks, eager handling means you pay the configuration cost for tasks you never execute, ballooning configuration-phase time. The remedy: register lazily, prefer `named`/`configureEach` over `getByName`/`all`, and wrap inputs in Providers. Laziness is also a hard prerequisite for the **configuration cache**, which can only serialize a graph that wasn't forced into eager evaluation.
code
kotlin · 6 lines// Lazy — preferred
val docs = tasks.register<Copy>("copyDocs") {
from("src/docs"); into(layout.buildDirectory.dir("docs"))
}
tasks.named("assemble") { dependsOn(docs) }
tasks.withType<Test>().configureEach { useJUnitPlatform() }go deeper
Know register/named are lazy and create/getByName are eager.
Explain realization and that eager handling configures tasks you never run.
Quantify impact on configuration time in multi-module builds and prescribe configureEach/Provider patterns.
Drive org-wide config-cache adoption: lint/ban eager APIs in convention plugins, measure config time via build scans, set build-performance SLAs.
## Eager vs lazy APIs | Concern | Eager (avoid) | Lazy (prefer) | |---|---|---| | Create task | `tasks.create("x")` | `tasks.register("x")` | | Configure existing | `tasks.getByName("y") { }` | `tasks.named("y") { }` | | Configure many | `tasks.withType(T).all { }` | `tasks.withType(T).configureEach { }` | | Get reference | `Task` (realized) | `TaskProvider<T>` (deferred) | ## What 'realization' means A *registered* task is just a recipe (a `TaskProvider`). It becomes a real `Task` object — **realized** — only when something needs it: it's in the requested task graph, or another realized task references it, or eager code touches it. Realization runs the task's configuration closure. ```kotlin val gen = tasks.register("generate") { /* not run unless needed */ } tasks.register("build2") { dependsOn(gen) // realizes 'generate' only because build2 depends on it AND build2 runs } ``` ## Why eagerness hurts The configuration phase runs on **every** invocation. If you eagerly `create`/`getByName` tasks, you configure them every time — even `./gradlew help`. In a build with hundreds of tasks across modules (common with many plugins), this dominates configuration time. A single `tasks.getByName("compileJava")` in a convention plugin can cascade into realizing related tasks. Measurable symptoms: slow `--dry-run`, high configuration time in `--profile`/build scans even for trivial tasks. ## The fixes 1. **Register, don't create**: `tasks.register`. 2. **named over getByName**, **configureEach over all/each**. 3. **Don't call .get()** on a `TaskProvider` during configuration unless required (it forces realization). 4. **Lazy inputs**: `Provider`/`Property` so the value (and any work to compute it) is deferred. ```kotlin // eager: forces realization of every Jar task now tasks.withType(Jar::class.java).all { archiveBaseName.set("app") } // lazy: configures each only when realized tasks.withType(Jar::class.java).configureEach { archiveBaseName.set("app") } ``` ## Connection to the configuration cache The **configuration cache** serializes the configured task graph to skip the configuration phase on later builds. It can only do this when configuration is side-effect-light and lazy — eager realization and reading mutable project state at configuration time are exactly what break it. So laziness isn't just micro-optimization; it's the gateway to the biggest configuration-time win.
- Does dependsOn(taskProvider) immediately realize the dependency task?No — passing a TaskProvider keeps it lazy; the dependency is realized only if the depending task is itself realized/executed. Passing a realized Task or calling .get() would force it.
- Why is configureEach preferred over all() / each()?`all`/`each` realize every matching task immediately to apply the closure; `configureEach` applies the closure lazily only when (and if) each task is realized, avoiding needless configuration cost.
- How does laziness relate to the configuration cache?The configuration cache serializes the configured graph to skip configuration on later builds; it requires lazy, side-effect-free configuration. Eager realization and configuration-time state reads break cache compatibility.
Eager is cooking every dish on the menu before knowing what anyone ordered; lazy is cooking only the dishes that get ordered.
saying these in an interview costs you the question
- Saying tasks.create and tasks.register behave identically
- Calling .get() on a TaskProvider during configuration without reason
- Using tasks.withType(...).all to apply config broadly