Why are convention plugins preferred over subprojects { }/allprojects { } for sharing build logic?
answer
- self-applied -> isolation + config-cache safe
- plugins { } = explicit, opt-in, documents the module
- composable + selectively applied
- real code: testable, versioned, reusable
- cost = upfront build-logic structure
basics
~20 sConvention plugins are applied by each project to itself, so they preserve project isolation, stay configuration-cache friendly, are explicit and opt-in per project, and are testable/versioned — unlike cross-config that mutates projects from the root.
solid answer
~50 sConvention plugins win because of **isolation and intent**, not just DRY. With `subprojects { }` the root mutates every child, which breaks project isolation and the configuration cache's per-project invariant, and forces *every* subproject to receive the same config whether it fits or not. A **convention plugin** packages the shared logic and each project **applies it to itself** via the `plugins { }` block. That means: (1) isolation holds — only the project's own applied code mutates it, so the configuration cache and parallel configuration work cleanly; (2) it's **explicit and opt-in** — a project's `plugins { }` block documents exactly which conventions apply, instead of magic inherited from the root; (3) it composes — a module can apply `java-conventions` plus `publishing-conventions` selectively; (4) it's **reusable and testable** — the plugin lives in `build-logic`, can be unit/functionally tested, versioned, and even shared across repos. The trade-off is more upfront structure (a `build-logic` included build), but it scales far better as the build grows.
code
kotlin · 12 lines// Convention plugin (self-applied) composes cleanly per module:
// :api module
plugins {
id("myorg.java-conventions")
id("myorg.publishing-conventions")
}
// :cli module — opts OUT of publishing, opts IN to application
plugins {
id("myorg.java-conventions")
id("myorg.application-conventions")
}go deeper
Know that convention plugins are applied per project via plugins { } and are the recommended replacement for shared cross-config.
List the concrete benefits — isolation, opt-in clarity, composition, testability — versus cross-config.
Connect to config cache/Project Isolation and discuss the structural trade-offs and when each makes sense.
Define an org convention-plugin platform (versioned, cross-repo) and the governance/CI that keeps cross-config out.
## The two ways to share logic **Cross-configuration** (`subprojects { }`, `allprojects { }`) sits in the *root* script and mutates children. A **convention plugin** is a reusable plugin (a precompiled `*.gradle.kts` script plugin, or a `Plugin<Project>` class) — usually in an included **`build-logic`** build — that a project **applies to itself**. ```kotlin // build-logic/src/main/kotlin/myorg.java-conventions.gradle.kts plugins { java } java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } // each subproject build.gradle.kts: plugins { id("myorg.java-conventions") } ``` ## Why convention plugins are preferred ### 1. Isolation & configuration cache Because the project applies the plugin to **itself**, the only code mutating it is its own. That keeps the per-project configuration invariant intact, so the **configuration cache** can reuse each project independently and **Project Isolation** can configure projects in parallel. Cross-config breaks exactly this. ### 2. Explicit & opt-in A project's `plugins { }` block is a **declaration of what it is** — "I'm a Java library that publishes." With `subprojects { }`, configuration is inherited invisibly from the root; you must read the root to know what any module does, and you can't easily exempt a module that shouldn't get the config (leading to brittle `if (project.name != "...")` guards). ### 3. Composition Conventions compose: a module can apply `java-conventions` + `publishing-conventions` and skip `application-conventions`. Cross-config tends toward one-size-fits-all blocks plus exception branches. ### 4. Reusable, testable, versioned A convention plugin is real code: it can be **unit-/functionally tested** (e.g. with `GradleRunner`/TestKit), versioned, and even published to share across repositories. `subprojects { }` is anonymous script glue that can't be tested or reused outside that one root. ### 5. Better lazy APIs Convention plugins encourage proper lazy configuration (`tasks.register`, `Provider`/`Property`, `configureEach`) scoped to one project, instead of broad eager iteration over `subprojects`. ## The cost You pay an upfront structural cost: create a `build-logic` included build (`includeBuild("build-logic")` / `pluginManagement`), add the `kotlin-dsl`/`java-gradle-plugin`, and write the plugin. For a small two-module build that's overkill; for anything that will grow, it pays back quickly. (The mechanics of *where* convention plugins live — buildSrc vs included build-logic, organizing them — are separate concerns.)
- Are convention plugins always the right call, even for a 2-module project?Not necessarily. For a tiny build the build-logic setup is overhead; subprojects { } may be pragmatic. The benefits (isolation, testability, opt-in clarity) compound as the number of modules and conventions grows.
- How do you handle a module that needs slightly different config under convention plugins?Apply the base convention and override locally in that module's own script, or factor a second convention plugin. You avoid the if (project.name == ...) branching that cross-config forces.
- Can convention plugins be tested?Yes — as real plugins they can be exercised with Gradle TestKit (GradleRunner) in functional tests, and class-based plugins can be unit tested. Cross-config script blocks can't.
Cross-config is a building-wide announcement everyone is forced to hear; a convention plugin is a labeled handbook each team picks up only if it applies to them.
saying these in an interview costs you the question
- Arguing they're identical and only differ in style
- Claiming convention plugins can't be tested
- Recommending build-logic for every trivial build without weighing the overhead