What kinds of work should you avoid doing eagerly in Plugin<Project>.apply, and what should you do instead?
answer
- apply = configuration time, every build
- no resolve()/.files, no I/O, no exec
- register over create, wire providers
- real work in @TaskAction
- avoid afterEvaluate crutch
basics
~20 sDon't do expensive or side-effecting work in apply — no I/O, no resolving configurations, no eager tasks.create or forcing Property.get(). Register tasks lazily, wire Providers, and defer real work to task actions executed only when needed.
solid answer
~40 s`apply` runs at **configuration time**, on every build invocation, before Gradle knows which tasks will run. So it must be cheap and side-effect-free. Avoid: eager `tasks.create` (use `register`); resolving dependency configurations (`configuration.resolve()`/`.files`) which triggers downloads; reading files, running processes, or network calls; calling `Property.get()`/`provider.get()` to snapshot values early; and `project.afterEvaluate { }` as a crutch. Instead: register tasks and their inputs lazily, wire `Provider`s rather than values, and put all real work inside `@TaskAction` so it only runs for tasks actually scheduled (and benefits from up-to-date checks and caching). Heavy eager work in `apply` slows *every* invocation — even `gradle help` — and is amplified across hundreds of subprojects. Configuration avoidance (`register`, `named`, `withType` reacting) keeps configuration time flat.
code
kotlin · 15 lines// BAD: eager work at configuration time
override fun apply(project: Project) {
val files = project.configurations.getByName("runtimeClasspath").files // resolves now!
val task = project.tasks.create("report", ReportTask::class.java) // eager
task.title.set(project.extensions.getByType(ReportExtension::class.java).title.get()) // eager get
}
// GOOD: lazy, deferred to execution
override fun apply(project: Project) {
val ext = project.extensions.create("report", ReportExtension::class.java)
project.tasks.register("report", ReportTask::class.java) { t ->
t.title.set(ext.title)
t.classpath.from(project.configurations.named("runtimeClasspath"))
}
}go deeper
Know real work goes in the task action, not apply; use register.
List eager pitfalls (create, .files, .get(), I/O) and the lazy alternatives.
Explain configuration vs execution phases, configuration avoidance, and the multi-project cost amplification.
Drive profiling (build scans/--profile) and standards so configuration time stays flat as the build grows.
## apply runs at configuration time, always Gradle has distinct phases: **initialization → configuration → execution**. A plugin's `apply` runs in the **configuration** phase, which executes on *every* invocation regardless of which tasks are requested. Anything slow here taxes every command, including no-op ones like `gradle help` or `gradle tasks`. In a multi-project build with hundreds of subprojects each applying the plugin, the cost multiplies. ## What to avoid in apply 1. **Eager task creation** — `tasks.create(...)` instantiates and configures the task immediately. Prefer `tasks.register(...)` which returns a `TaskProvider` and only realizes the task if it's in the task graph (configuration avoidance). 2. **Resolving configurations** — calling `configuration.files`, `.resolve()`, `.incoming.artifacts` at configuration time forces dependency resolution and possibly downloads, dramatically slowing configuration. Pass the configuration as a lazy `FileCollection`/`Provider` to the task instead and let it resolve at execution. 3. **Eager value reads** — `extension.someProperty.get()` snapshots the value before users configure the extension; it's both a correctness bug and forces eager evaluation. Wire providers with `.set(provider)`. 4. **I/O and side effects** — reading/writing files, spawning processes (`exec`), HTTP calls. These belong in `@TaskAction`, gated by up-to-date checks. 5. **afterEvaluate as a band-aid** — using `project.afterEvaluate { ... }` to "wait" for configuration is usually a symptom of not using lazy Providers; prefer Providers so ordering doesn't matter. ## What to do instead ```kotlin override fun apply(project: Project) { val ext = project.extensions.create("report", ReportExtension::class.java) val deps = project.configurations.named("runtimeClasspath") // lazy, no resolve project.tasks.register("report", ReportTask::class.java) { t -> t.title.set(ext.title) // lazy provider t.classpath.from(deps) // resolved at execution t.output.set(project.layout.buildDirectory.file("report.txt")) } } ``` All real computation lives in `ReportTask`'s `@TaskAction`, so it runs only when `report` is scheduled, participates in incremental build / build cache, and keeps `apply` to microseconds. ## Why it matters Configuration time is paid up front on every build; lazy APIs (`register`, `named`, `withType`, Providers) are Gradle's **configuration avoidance** machinery. Plugins that respect it scale to large builds; plugins that do eager work become the bottleneck everyone profiles with `--profile` or build scans.
- Why is calling configuration.files in apply harmful?It forces dependency resolution (and possibly downloads) at configuration time on every build, even when the task that needs them won't run. Pass the configuration lazily so it resolves only at task execution.
- How do register and named support configuration avoidance?register/named return Providers and defer the configuration lambda until the task is actually realized (in the task graph), so unrequested tasks cost nothing to configure.
- When is afterEvaluate ever justified?Rarely — only when you genuinely need the fully-configured project state and can't express it lazily. Modern plugins almost always replace it with Provider wiring, which is order-independent.
apply() is like setting the table before you know who's coming for dinner — lay out place settings (register tasks) cheaply, but don't start cooking the meal (do I/O / resolve deps) until someone actually sits down (the task runs).
saying these in an interview costs you the question
- Resolving dependencies or doing I/O in apply.
- Calling Property.get() during configuration to read user values.
- Reaching for afterEvaluate instead of lazy Providers.
- Defending tasks.create as equivalent to register.