Why is cross-configuring child modules via allprojects{}/subprojects{} in the root build considered a coupling problem, and what does it cost you?
answer
- ownership inverted: child config in root
- module no longer self-describing
- root coupled to whole tree
- eager config vs configuration cache
- convention plugins = opt-in, local, portable
basics
~20 sEach child's build is partly defined in a file it doesn't own (the root), so you can't read a module's script and know its real config. It also forces every module to be configured whenever any is touched, hurting maintainability and laziness.
solid answer
~50 s`allprojects {}`/`subprojects {}` invert ownership: a subproject's plugins, dependencies and settings live in the *root* script, not the subproject's own `build.gradle.kts`. The concrete costs: - **Opaque modules** — reading `:lib/build.gradle.kts` no longer tells you what `:lib` actually does; you must also read the root. - **Unconditional reach** — the root must configure *all* targets, so it implicitly depends on every child, and every child implicitly depends on the root's logic. Adding a module silently inherits everything. - **Eager, whole-tree configuration** — these blocks run during the configuration phase against every project, which works against configuration avoidance and confuses configuration caching when the logic touches mutable cross-project state. - **No reuse across builds** — the logic is trapped in one root script; another repo can't apply it. The accepted replacement is **convention plugins** (in `buildSrc` or an included `build-logic` build): each module *applies* the convention it wants, keeping ownership local and the dependency explicit. That's why modern Gradle guidance steers away from cross-configuration.
code
kotlin · 13 lines// BEFORE (coupled): root build.gradle.kts owns every child
subprojects {
apply(plugin = "java-library")
repositories { mavenCentral() }
}
// AFTER (decoupled): a convention each module opts into
// build-logic/src/main/kotlin/myproject.java-conventions.gradle.kts
plugins { `java-library` }
repositories { mavenCentral() }
// :payments/build.gradle.kts
plugins { id("myproject.java-conventions") }go deeper
Recognize that putting child config in the root means a module isn't self-describing; basic awareness is enough.
Name the concrete costs (opaque modules, unconditional reach) and that convention plugins are the fix.
Connect to configuration avoidance and the configuration cache, and articulate the opt-in/local/portable wins of convention plugins.
Frame it as a governance/scaling decision: enforce convention plugins org-wide, keep root scripts thin, and treat cross-configuration as tech debt to migrate.
## What "coupling" means here Normally a Gradle project should be self-describing: its `build.gradle.kts` lists its plugins, dependencies, and settings. `allprojects {}`/`subprojects {}` break that by letting the root reach in and configure children. Now a module's effective configuration is the *union* of its own script and whatever the root applies to it. The root and every child become mutually coupled. ## The concrete costs ### 1. You can't reason about a module locally Open `:payments/build.gradle.kts` and you might see only a couple of dependencies — but the module also has the `java-library` plugin, JUnit, a repository list, and a `group` it inherited from the root. To understand the module you must read two files and mentally merge them. Multiply by many modules and onboarding/debugging slows. ### 2. The root depends on the whole tree Because `subprojects {}` is unconditional, the root logic must make sense for *every* subproject. A new module silently inherits everything; a module that doesn't fit needs an `if` exception in the root, accreting special cases. The root build script becomes a god object. ### 3. It fights laziness and configuration cache Cross-configuration runs **eagerly** during configuration of the root, configuring projects you may not be building. It works against task/extension configuration avoidance, and patterns that read or mutate another project's state at configuration time (`project(":other").property`) are exactly what the **configuration cache** flags as cross-project access problems, because the cache wants each project configured in isolation. ### 4. Zero portability The shared logic lives in one repository's root script. You can't publish it or apply it from another build. Convention plugins, by contrast, can live in an included `build-logic` build and even be published as a plugin. ## The replacement ```kotlin // build-logic/src/main/kotlin/myproject.java-conventions.gradle.kts plugins { `java-library` } repositories { mavenCentral() } dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") } // :payments/build.gradle.kts plugins { id("myproject.java-conventions") } ``` Now `:payments` *opts in* by applying a named convention. Ownership is local (the apply is visible in the module), membership is explicit (no module is configured unless it applies the plugin), the typed DSL works because the plugin is applied directly, and the logic is reusable across builds. This is the trade-off interviewers want you to articulate: cross-configuration is convenient for tiny builds but couples the tree; convention plugins keep modules isolated and self-describing.
- How does cross-configuration interact badly with the configuration cache?The configuration cache assumes each project is configured in isolation. Cross-configuration that reads or mutates another project's state at configuration time (e.g. `project(":x").someProp`) is flagged as a cross-project access problem and can invalidate or break caching, whereas convention plugins keep each project self-contained.
- Doesn't `allprojects { group = ... }` for a shared coordinate seem harmless? Is it ever acceptable?Setting a uniform `group`/`version` is the least harmful use because it's a simple inherited property, not behavior. Many teams tolerate it. But it's still cross-configuration; the cleaner equivalent is a base convention plugin or settings-level configuration, and once you add plugins/dependencies the coupling cost grows quickly.
- What two homes can convention plugins live in, and how do they differ?`buildSrc` (auto-available to the main build, but a change recompiles and can invalidate the whole build) or an included `build-logic` build (referenced via `includeBuild`, more isolated and publishable). Both let modules apply named conventions instead of being cross-configured.
Cross-configuration is like a building manager who walks into every apartment and rearranges the furniture from a master plan — tidy at first, but no tenant can read their own lease to know what's in their room, and one change ripples through every unit. Convention plugins are pre-set furniture packages each tenant chooses to order.
saying these in an interview costs you the question
- Saying cross-configuration has no real downside / is just style.
- Claiming convention plugins are slower or more complex with no benefit — they restore module isolation and reuse.
- Confusing this with multi-module being bad; the problem is *where* config lives, not having many modules.