You used tasks.register to keep a task lazy, but it still gets configured on every build. What kinds of code force a registered task to be realized?
answer
- realization = instantiate + configure
- get(), getByName, all{}, iteration force it
- configureEach / named are lazy
- cascade: realizing A may realize B
- diagnose with --scan / --profile
basics
~10 sEager APIs force realization: tasks.getByName, tasks.create, tasks.all {}, iterating the task container, or calling .get() on a provider. Any of these realizes lazy tasks, undoing the benefit of register.
solid answer
~40 sRegistering a task is only half the battle — anything that *touches the concrete task object* forces it to be realized (instantiated + configured). Common culprits: calling `taskProvider.get()` eagerly; `tasks.getByName("x")`; `tasks.all { ... }` or `tasks.withType(Foo) { ... }` *without* the lazy `configureEach` form; iterating the container (`tasks.forEach`, `tasks.toList()`); or referencing a task's output via `someTask.outputs` instead of a lazy `Provider`. Even one eager call can cascade: realizing task A may pull in B if A's config references B's concrete instance. The fix is to keep the whole wiring lazy — use `tasks.named("x")`, `withType(Foo).configureEach { }`, and pass `Provider`/`TaskProvider` outputs instead of resolved values. To diagnose, a build scan or `--info` shows which tasks were created; ideally only the requested graph is realized.
code
kotlin · 10 lines// LEAK: eager withType realizes every JavaCompile immediately
tasks.withType(JavaCompile::class.java) { options.encoding = "UTF-8" }
// FIX: configureEach keeps it lazy
tasks.withType(JavaCompile::class.java).configureEach { options.encoding = "UTF-8" }
// LEAK: get() resolves now
val out = myTask.get().archiveFile
// FIX: stay lazy with flatMap
val outProvider = myTask.flatMap { it.archiveFile }go deeper
Know that calling .get() or getByName resolves a task immediately; just recognize the term realization.
Enumerate the common eager APIs and their lazy equivalents, and explain the cascade so a single leak can realize many tasks.
Diagnose leaks with build scans/profiling and refactor wiring to flatMap/Provider so outputs stay lazy across task boundaries.
Establish guardrails (custom lint, code review rules, configuration-cache enforcement) so shared build logic never reintroduces eager realization at scale.
## Realization defined A registered task lives as a `TaskProvider` until it is **realized** — the moment Gradle actually instantiates the task and runs its configuration block. The goal of `register` is to keep tasks unrealized unless they're on the path to what the user requested. **Realization leaks** are code paths that force creation prematurely. ## What forces realization - **`provider.get()`** — resolves the provider to the concrete `Task` now. - **Eager lookups:** `tasks.getByName("x")`, `tasks.getByPath(...)`, `project.task("x")`. - **Eager bulk APIs:** `tasks.all { }`, `tasks.withType(Foo) { }` (the *closure-taking* eager overload), `tasks.matching { }.all { }`. - **Iteration:** `tasks.forEach`, `tasks.toList()`, `tasks.size` in some paths, `for (t in tasks)`. - **Eager wiring:** reading `otherTask.outputs.files` / `otherTask.someProperty` directly instead of via a `Provider`. - **`create`** anywhere, and legacy plugins that call these internally. ## The lazy equivalents | Eager (forces realize) | Lazy (safe) | |---|---| | `tasks.getByName("x")` | `tasks.named("x")` | | `tasks.all { }` | `tasks.configureEach { }` | | `tasks.withType(Foo) { }` | `tasks.withType(Foo).configureEach { }` | | `provider.get().output` | `provider.flatMap { it.output }` | | `tasks.create("x")` | `tasks.register("x")` | ## Cascade effect Realization is transitive. If you realize task A and A's configuration block calls `tasks.getByName("B")`, B is realized too. In a plugin-heavy build a single eager `withType` in `build.gradle` can realize dozens of tasks. That's why the advice is to keep *every* link in the chain lazy. ## Diagnosing ```bash # Build scan shows a "task creation" timeline ./gradlew help --scan # or look at configuration time ./gradlew help --profile ``` If `gradle help` realizes tasks unrelated to help, you have a leak. Gradle's own configuration-cache and `--info` logging can also surface unexpected creations. ## Takeaway `register` gives you laziness; an eager API anywhere in the wiring takes it away. Audit for `create`, `getByName`, `all`, `withType(...) { }` (no `configureEach`), and direct iteration.
- Why is tasks.configureEach lazier than tasks.all?configureEach defers the configuration action and applies it only as each task is realized; all eagerly realizes and configures every current and future task immediately.
- How would you find which task caused an unexpected realization?Run with --scan and inspect the configuration/task-creation timeline, or --profile for configuration time; bisect by removing suspect withType/getByName calls until gradle help stops realizing extra tasks.
- Does tasks.named force realization?No — named returns a TaskProvider and the configuration action you pass runs only when the named task is itself realized.
saying these in an interview costs you the question
- Believing register alone guarantees laziness regardless of how you wire the task.
- Confusing withType(...) {} (eager) with withType(...).configureEach {} (lazy).
- Thinking calling .get() in configuration is free.