skip to content

Why are convention plugins preferred over subprojects { }/allprojects { } for sharing build logic?

level: middleimportance: must knowfreq 50%

answer

  1. self-applied -> isolation + config-cache safe
  2. plugins { } = explicit, opt-in, documents the module
  3. composable + selectively applied
  4. real code: testable, versioned, reusable
  5. cost = upfront build-logic structure

basics

~20 s

Convention 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 s

Convention 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
kotlin
// 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

for a junior

Know that convention plugins are applied per project via plugins { } and are the recommended replacement for shared cross-config.

for a middle

List the concrete benefits — isolation, opt-in clarity, composition, testability — versus cross-config.

for a senior

Connect to config cache/Project Isolation and discuss the structural trade-offs and when each makes sense.

for a principal

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

context