What are the practical pitfalls of declaring ordering rules — eager vs lazy task references, accepting TaskProvider, and configuration-time cost — when wiring mustRunAfter/shouldRunAfter at scale?
answer
- accepts name / TaskProvider / Task
- getByName = eager realization
- configure inside named { }
- configureEach not all { }
- cost = realization, not the edge
basics
~20 sPrefer passing TaskProviders or task names to mustRunAfter/shouldRunAfter and configure inside tasks.named { }, so you don't eagerly realize tasks at configuration time. Eagerly resolving with tasks.getByName(...) forces unnecessary task creation and hurts configuration performance.
solid answer
~40 s`mustRunAfter` and `shouldRunAfter` accept the same flexible argument types as `dependsOn`: task names (`String`), `Task` instances, `TaskProvider`s, or even providers/closures resolved lazily. The pitfall at scale is **eager realization**: if you write `tasks.getByName("a").mustRunAfter(tasks.getByName("b"))`, you force both task objects to be *created and configured* during the configuration phase, even if neither will run. With the configuration-avoidance API (`tasks.register` + `tasks.named`), tasks are only realized on demand. Best practice: - Configure the rule inside `tasks.named("a") { mustRunAfter("b") }` so `a` is realized only if needed. - Pass the *name* or a `TaskProvider` rather than a realized `Task`, so the target stays lazy. - Avoid `getByName`/`all { }` patterns that defeat configuration avoidance. Getting this wrong shows up as slower configuration time across many modules, even though the rules themselves are cheap.
code
kotlin · 11 lines// Lazy, configuration-avoidance-friendly ordering wiring
val test = tasks.register("test")
val integTest = tasks.register("integTest")
tasks.named("integTest") {
// pass the name or the provider; neither forces realization of 'test'
mustRunAfter(test) // TaskProvider — stays lazy
// shouldRunAfter("slowCheck") // by name is equally lazy
}
// Avoid: tasks.getByName("integTest").mustRunAfter(tasks.getByName("test"))go deeper
Just know the methods accept task names or task references.
Know that names/providers are preferable to realized Task objects and why.
Tie ordering-rule wiring to configuration avoidance (register/named/configureEach) and explain the realization cost at multi-module scale.
Set plugin-authoring conventions that keep ordering rules lazy across the build so configuration time stays flat as modules grow.
## Why argument types matter The ordering methods are overloaded to take *anything that can resolve to a task*: - `String` task name — resolved lazily at execution-graph time. - `TaskProvider<T>` — the configuration-avoidance handle from `tasks.register(...)`; stays lazy. - `Task` — already realized; passing it **forces realization**. - `Provider<...>` / collections of the above. Because the target only needs to be *identified* (not configured) to establish an ordering edge, passing a name or provider lets Gradle defer realizing the target until it actually decides to schedule it. ## The configuration-avoidance angle Gradle's configuration-avoidance API (`register`/`named`) exists so tasks that aren't part of the requested build are never created or configured. Two common mistakes silently break this: ```kotlin // BAD: getByName eagerly realizes BOTH tasks at configuration time tasks.getByName("integTest").mustRunAfter(tasks.getByName("test")) // GOOD: stays lazy — integTest realized only if configured/scheduled, // "test" referenced by name so it isn't forced either tasks.named("integTest") { mustRunAfter("test") } ``` At the scale of dozens of subprojects each adding a handful of ordering rules in a convention plugin, the eager pattern can add measurable wall-clock time to every invocation — including `gradle help` — because configuration runs regardless of which tasks you asked for. ## Practical guidance 1. **Always configure inside `named { }`**, never `getByName` for the subject task. 2. **Reference targets by name or `TaskProvider`**, not realized `Task` objects. 3. **Avoid `tasks.all { }` / `withType { }.all`** when adding ordering rules — they realize the whole collection. Prefer `withType<T>().configureEach { }`. 4. Remember the rule itself is *conditional and cheap*; the cost you're guarding against is task **realization**, not the ordering edge. ## Subtle correctness note Because ordering rules are evaluated against the final graph, you don't need the target to exist *yet* when you declare the rule by name — a lazily-registered task with that name is fine. This is another reason to prefer string/provider references over realized tasks. ## Checklist - Subject configured via `named { }`? ✅ - Target passed as name/provider, not realized Task? ✅ - No `getByName`/`.all { }` realizing the world? ✅
- Does passing a task *name* to mustRunAfter require that task to already exist at configuration time?No. The name is resolved when the execution graph is built, so a task registered lazily elsewhere with that name is fine — another reason to prefer names/providers over realized Task instances.
- How does using `tasks.all { mustRunAfter(...) }` hurt performance?`all { }` eagerly realizes and configures every task in the collection at configuration time, defeating configuration avoidance. Use `configureEach { }` instead, which is lazy.
- Is the ordering edge itself expensive?No — the edge is cheap and conditional. The cost to watch for is eager task *realization* caused by how you reference the tasks.
saying these in an interview costs you the question
- Recommending tasks.getByName for wiring ordering at scale.
- Thinking the ordering rule itself is the performance cost rather than task realization.
- Using all { } instead of configureEach { } in convention plugins.