Beyond isolation, what eager-evaluation and ordering pitfalls does cross-configuration cause, and how do convention plugins avoid them?
answer
- subprojects { } eagerly iterates all projects
- afterEvaluate cross-project = order-dependent + fragile
- use pluginManager.withPlugin within the project
- lazy: register / configureEach / Provider
- convention plugin runs in target project, in order
basics
~10 ssubprojects { } eagerly visits every project, often forcing afterEvaluate hooks and order-dependent code that's fragile. Convention plugins configure one project lazily with configureEach/register, removing the ordering traps.
solid answer
~50 sCross-config has runtime pitfalls beyond the isolation principle. `subprojects { }`/`allprojects { }` **eagerly iterate** all projects at root-configuration time, which realizes project models early and can defeat lazy configuration. Because the root often needs a child's plugins or extensions to already be present, people wrap the cross-config in **`afterEvaluate { }`**, creating brittle, order-dependent code: the behavior now depends on evaluation order between root and children, and stacked `afterEvaluate` hooks are notoriously hard to reason about. You also tend to eagerly resolve things (configurations, task references) inside these blocks. **Convention plugins** sidestep all of it: the plugin runs **within the target project's own configuration**, so the plugins/extensions it needs are applied in-order locally, no `afterEvaluate` cross-project dance is needed, and you can use the **lazy** APIs — `tasks.register`, `tasks.withType<T>().configureEach`, `Provider`/`Property` — scoped to that one project. The result is faster, more deterministic configuration that plays well with the configuration cache.
code
kotlin · 14 lines// Fragile cross-config: defer with afterEvaluate to wait for the child's plugin
subprojects {
afterEvaluate {
if (plugins.hasPlugin("java")) {
tasks.withType<Test>().configureEach { useJUnitPlatform() }
}
}
}
// Convention plugin: deterministic, lazy, no cross-project afterEvaluate
// myorg.test-conventions.gradle.kts (applied by each project)
pluginManager.withPlugin("java") {
tasks.withType<Test>().configureEach { useJUnitPlatform() }
}go deeper
Know that subprojects { } iterates eagerly and that afterEvaluate-based shared config is fragile.
Explain why ordering makes cross-project afterEvaluate brittle and name the lazy APIs.
Contrast pluginManager.withPlugin + configureEach in a convention plugin with the afterEvaluate trap, and tie to config-cache/perf.
Set lazy-configuration standards and lint against eager/afterEvaluate cross-config across the org's build-logic.
## The hidden costs of cross-config at runtime The isolation argument is the headline, but cross-configuration also creates concrete **eager-evaluation** and **ordering** problems. ### Eager iteration `subprojects { }` and `allprojects { }` iterate **all** projects when the root is configured. That eagerly realizes each project's model and frequently triggers eager work inside the block (resolving a configuration, touching a task by name). Modern Gradle pushes **lazy** configuration — create tasks with `tasks.register` (not `tasks.create`), mutate with `configureEach` (not `all`/eager `withType{}` realization), and carry values in `Provider`/`Property` so they're computed only when needed. Broad cross-config blocks cut against this grain. ### The afterEvaluate trap Cross-config often needs a child's state that isn't ready yet. For example, configuring something that depends on a plugin a child applies in *its own* script: ```kotlin subprojects { // The child applies 'java' in its own build.gradle.kts, not yet visible here, // so people defer: afterEvaluate { if (plugins.hasPlugin("java")) { extensions.configure<JavaPluginExtension> { /* ... */ } } } } ``` This is fragile: - Behavior depends on **evaluation order** (root vs each child, and the order children are evaluated). - **Stacked `afterEvaluate`** blocks run in registration order and interact in non-obvious ways. - It's easy to read or mutate values **too early or too late**, producing heisenbugs. ### Eager resolution Resolving a `Configuration` or reading a computed value inside a cross-config block at configuration time can force dependency resolution early and pull mutable state into the model — bad for both performance and the configuration cache. ## How convention plugins remove the pitfalls A convention plugin runs **inside the project that applies it**, so: - The `plugins { }` block of that project establishes plugin presence **in order, locally** — no `afterEvaluate` needed to wait for a plugin, and no cross-project ordering question. You can react to other plugins with `pluginManager.withPlugin("java") { ... }` *within the same project*, which is deterministic. - You use the **lazy** APIs naturally and scoped to one project: ```kotlin // myorg.test-conventions.gradle.kts pluginManager.withPlugin("java") { tasks.withType<Test>().configureEach { useJUnitPlatform() } } ``` - No eager iteration over `subprojects`; each project configures itself once, lazily. The payoff is **deterministic, lazier, faster** configuration that the configuration cache can serialize and reuse per project. So the convention-plugin preference isn't only philosophical isolation — it removes a class of real ordering bugs and performance drags.
- Why is pluginManager.withPlugin preferable to afterEvaluate for reacting to a plugin?withPlugin fires deterministically when (and if) that plugin is applied to the SAME project, regardless of evaluation order, and stays within isolation. afterEvaluate depends on global evaluation ordering and crosses projects in the subprojects { } case.
- What's the difference between tasks.all/withType{} eager and configureEach here?all and an eager withType { } realize matching tasks immediately; configureEach defers the configuration action until each task is actually realized, avoiding unnecessary eager work — important inside shared logic that touches many tasks.
saying these in an interview costs you the question
- Treating afterEvaluate as a normal, safe pattern for shared config
- Using tasks.create / eager withType {} in shared logic
- Assuming evaluation order between root and subprojects is guaranteed