In a large multi-project build, how would you drive down configuration time using the avoidance API and verify the improvement?
answer
- convention plugins multiply cost
- grep create/getByName/all
- provider wiring + flatMap
- build scan / --profile diff
- CC readiness + CI budget
basics
~10 sReplace eager APIs (create/getByName/all/withType-closure) with register/named/configureEach across convention plugins, keep dependencies provider-based, then measure config time with build scans or --profile before/after.
solid answer
~50 sAt scale the leverage is in shared **convention plugins**: a single eager `tasks.withType<JavaCompile> { }` or `getByName` there multiplies across every module on every invocation, including IDE sync. The playbook: (1) audit convention/build-logic plugins and replace `create→register`, `getByName→named`, `all/withType{}→configureEach`; (2) keep all task wiring provider-based (`dependsOn(provider)`, `flatMap` for outputs) so nothing realizes transitively; (3) make laziness a code-review gate for build-logic. Verify empirically: capture a **build scan** or `--profile` report for a representative command (e.g. `./gradlew help` and a common task) before and after, and compare configuration time and the count of realized tasks. Avoidance is also a stepping stone to the **configuration cache** — fewer realized tasks and provider-based wiring make the build CC-compatible, which then skips the configuration phase entirely. The governance angle: treat configuration time as a tracked metric with a regression budget in CI.
code
kotlin · 7 linesclass JavaConventions : Plugin<Project> {
override fun apply(project: Project) = with(project) {
// lazy, type-filtered: zero realization unless a build needs these tasks
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }
tasks.withType<Test>().configureEach { useJUnitPlatform() }
}
}go deeper
Out of depth; at most name register/configureEach as the lazy APIs.
Describe replacing eager APIs across modules and roughly how to measure config time.
Lay out the migration playbook with provider-based wiring and before/after measurement.
Frame it as an org program: convention-plugin focus, CI regression budget, CC readiness, and enforcement via review/lint.
## Where the cost concentrates In a monorepo, build logic lives in **convention plugins** (often in `build-logic`/`buildSrc`). Anything eager there runs *per module, per invocation*. One `tasks.all { }` across 300 modules can dominate configuration time and slow every IDE import. ## The migration program 1. **Inventory eager APIs.** Grep build-logic for `create(`, `getByName(`, `.all {`, and the closure `withType(...) {`. Each is a candidate. 2. **Mechanical replacement.** - `tasks.create` → `tasks.register` - `tasks.getByName` → `tasks.named` - `tasks.all { }` → `tasks.configureEach { }` - `tasks.withType(T) { }` → `tasks.withType(T).configureEach { }` 3. **Provider-based wiring.** Replace `dependsOn(p.get())` with `dependsOn(p)`; consume outputs via `from(producer.flatMap { it.someOutput })` and `Property`/`Provider` rather than reading realized task state. 4. **Eliminate cross-task eager reads.** Configuration blocks must not call `getByName`/iterate tasks. ## Measuring before/after - **Build scan** (`--scan`): the *Configuration* timeline and the list of configured/realized tasks. Compare scans pre/post. - **`--profile`**: writes an HTML report with a configuration-time breakdown per project. - A cheap proxy metric: realized-task count for `./gradlew help` — ideally near-zero custom tasks. ## Governance - Put a configuration-time / realized-task **regression budget** in CI; fail the build (or warn) if it grows. - Code-review checklist for build-logic: "no eager task APIs." - Consider a custom check/lint that flags `create`/`getByName`/`all` in build-logic sources. ## Strategic payoff: configuration cache The configuration cache stores the configured task graph and **skips the configuration phase** on cache hits. To be compatible, the build must avoid reading mutable state at execution time and should minimize unnecessary realization. So configuration avoidance is both an immediate win and the prerequisite that unlocks the much larger CC win. ```kotlin // A convention plugin done right — fully lazy class JavaConventions : Plugin<Project> { override fun apply(project: Project) = with(project) { tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" } tasks.withType<Test>().configureEach { useJUnitPlatform() } } } ```
- How do you prove the change actually reduced configuration time?Capture a build scan or --profile report for a representative invocation before and after, and compare the configuration-phase duration and the number of realized/configured tasks. A near-zero custom-task realization count for ./gradlew help is a strong signal.
- Why is configuration avoidance a prerequisite for the configuration cache paying off?The configuration cache stores the configured graph and skips the configuration phase on hits. Provider-based, low-realization builds are easier to make CC-compatible and produce smaller, cleaner cached state, so avoidance both reduces cold-config cost and unlocks the larger CC win.
- How would you stop config-time regressions from creeping back in?Track configuration time / realized-task count as a CI metric with a budget, add a build-logic code-review checklist banning eager task APIs, and optionally a custom lint that flags create/getByName/all in build-logic sources.
saying these in an interview costs you the question
- Optimizing leaf module scripts while ignoring the convention plugins where eager calls multiply.
- Claiming avoidance and the configuration cache are the same mechanism — avoidance reduces config work; CC skips the phase.