You inherit a build that shares config through a large subprojects {} block in the root. How would you migrate it to per-subproject convention plugin application, and what risks do you watch for?
answer
- audit & partition the block by concern
- map concerns -> plugin ids
- apply per module, strip root incrementally
- hidden reliance on broadcast = main risk
- verify with config cache / isolation flag
basics
~20 sGroup the subprojects {} config by concern, expose each group as a convention plugin id, then add the matching ids to each module's plugins {} block and delete the root block. Watch for modules that quietly relied on root-injected config.
solid answer
~50 sMigration is mechanical but risk-laden. First, audit what the `subprojects { }` block configures and split it by concern (toolchain/test, publishing, application). These become convention plugin ids. Then, module by module, add the right ids to each `plugins { }` block — not blanket, but matched to each module's role — and remove that config from the root. Finally delete the `subprojects {}` block. The main risk is **hidden reliance**: modules that worked only because the root injected something (a repository, a dependency, a task) will break the moment the broadcast stops, and the failure surfaces in a seemingly unrelated module. Mitigate by migrating incrementally — convert a few modules, keep them green, and use the Configuration Cache / Project Isolation feature flag to catch remaining cross-project access. Verify with `./gradlew build` per module and compare resolved configurations before/after. Edge cases: modules that genuinely differed under conditionals in the old block now need distinct convention plugins or explicit per-module overrides.
code
kotlin · 17 lines// BEFORE (root build.gradle.kts)
subprojects {
apply(plugin = "java")
repositories { mavenCentral() }
dependencies { testImplementation("org.junit.jupiter:junit-jupiter") }
if (name == "service") apply(plugin = "application")
}
// AFTER
// lib/build.gradle.kts
plugins { id("myproject.java-conventions") }
// service/build.gradle.kts
plugins {
id("myproject.java-conventions")
id("myproject.application-conventions")
}
// root: subprojects {} block removedgo deeper
Recognize the goal: move config from root subprojects {} into each module's plugins {} block.
Lay out the audit -> map -> apply -> strip sequence and name the hidden-reliance risk.
Drive incremental migration with verification via Configuration Cache / isolation flag and resolved-config diffs; handle conditionals.
Plan the migration as a build-platform program: sequencing, team coordination, governance to prevent subprojects {} creeping back.
## Why migrate at all A root `subprojects { }` block shares config by reaching into every child at configuration time. That cross-project access undermines the Configuration Cache and the experimental Project Isolation feature, and it makes each module non-self-describing. Replacing it with per-subproject convention plugin application restores isolation and local readability. ## Step-by-step ### 1. Audit and partition Read the `subprojects {}` (and `allprojects {}`) blocks and list every concern they configure: Java toolchain, repositories, test framework, publishing, the `application` plugin, lint, etc. Note any conditionals (`if (project.name == ...)`) — those are heterogeneous configs hiding inside a uniform block. ### 2. Map concerns to convention plugin ids Each coherent group becomes a convention plugin id you will *apply* (authoring lives elsewhere): `myproject.java-conventions`, `myproject.publishing-conventions`, `myproject.application-conventions`, etc. Conditional branches usually map to *separate* ids applied only where needed. ### 3. Apply per module, then strip the root For each module, add the matching ids: ```kotlin // lib/build.gradle.kts plugins { id("myproject.java-conventions") id("myproject.publishing-conventions") } ``` Then delete the corresponding lines from the root `subprojects {}`. Do this incrementally — a handful of modules per change — keeping the build green between steps. When the root block is empty, remove it. ### 4. Verify - Run `./gradlew build` (and key tasks) per migrated module. - Diff resolved dependencies/tasks before vs. after (e.g. `dependencies`, `tasks`) to catch drift. - Enable the Configuration Cache and, if available, the Project Isolation feature flag; both will *fail loudly* on any remaining cross-project access, which is exactly what you want to surface. ## Risks to watch - **Silent reliance on the broadcast.** A module may compile only because the root added `mavenCentral()` or a dependency. Remove the broadcast and it breaks — often in a module you didn't think you touched. Incremental migration localizes the blast radius. - **Conditional config.** `if (name == "service")` branches in the old block mean modules were never really uniform; don't flatten them into one convention. - **Order-sensitive config.** The old block applied things in a fixed order; composed plugins may apply differently. Keep plugin responsibilities non-overlapping. - **Over-application.** Resist creating one mega-convention to make migration trivial; that just relabels the original problem. ## Definition of done No `subprojects {}`/`allprojects {}` config sharing remains, every module's `plugins { }` block describes its role, the Configuration Cache hits, and per-module builds are green with unchanged resolved configurations.
- How do you catch modules that secretly depended on the old subprojects {} broadcast?Migrate incrementally and build each module; diff resolved configurations before/after; enable the Configuration Cache and Project Isolation flag, which fail loudly on remaining cross-project access.
- What do conditional branches inside the old subprojects {} block tell you?That modules were never uniform. Each branch usually becomes a separate convention plugin id applied only to the modules that need it, rather than being merged into one preset.
- Why migrate incrementally rather than all at once?To localize the blast radius. A big-bang removal of the root block can break many modules simultaneously and obscure which removed line caused which failure.
saying these in an interview costs you the question
- Deleting the whole subprojects {} block in one commit without verifying per module.
- Flattening conditional per-module config into a single shared convention.
- Creating one mega-convention applied everywhere to make migration trivial.