How does `apply false` in the root combine with precompiled convention plugins in a `buildSrc` or build-logic module to share plugin configuration across subprojects?
answer
- version vs behavior — two concerns
- convention plugin applies by id, no version
- version pinned via marker dep or root apply false
- subprojects apply only the convention plugin
- build-logic / buildSrc included build
basics
~20 sConvention plugins live in a separate build-logic module and apply third-party plugins by id (no version). The root apply false (or the build-logic module's dependency on the plugin marker) is what pins the version so the convention plugin can resolve it.
solid answer
~40 sThe clean layering is: a `build-logic` (or `buildSrc`) included build holds **precompiled script convention plugins** — e.g. `my-project.kotlin-conventions.gradle.kts` — that apply third-party plugins by id only and configure them once. Subprojects then apply just your convention plugin. For the convention plugin to apply `kotlin("jvm")` without a version, the version must be pinned. You do that either by adding the plugin's **marker dependency** to build-logic's `dependencies` (the version source for precompiled scripts), or, in the simpler root-build approach, by declaring the third-party plugin at the root with `apply false`. The `apply false` form keeps version selection in the consuming build while delegating *behavior* to convention plugins. In practice large builds prefer the build-logic + version-catalog route, but understanding `apply false` as the version-pinning primitive underneath both is the key insight.
code
kotlin · 15 lines// build-logic/src/main/kotlin/myorg.kotlin-conventions.gradle.kts
plugins {
id("org.jetbrains.kotlin.jvm") // no version — pinned upstream
}
kotlin { jvmToolchain(21) }
// root build.gradle.kts (Option B: pin via apply false)
plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.24" apply false
}
// app/build.gradle.kts
plugins {
id("myorg.kotlin-conventions")
}go deeper
Awareness only: convention plugins reuse configuration; apply false pins the version.
Explain that a convention plugin applies third-party plugins by id and that the version must be pinned (marker dep or root apply false).
Design the full layering: build-logic convention plugins for behavior, apply false / marker deps for version, subprojects consume one convention id.
Architect this across many repos: shared build-logic publishing, catalogs, governance of versions and conventions, configuration-cache and performance trade-offs.
## Two layers: version vs. behavior Sharing plugin setup across modules has two independent concerns: 1. **Version** — every module must use the same plugin version. 2. **Behavior** — every module should get the same configuration (toolchain, compiler args, test setup). `apply false` solves (1). **Convention plugins** solve (2). ## Convention plugins (precompiled script plugins) In a `build-logic` included build (or `buildSrc`), a file like `build-logic/src/main/kotlin/myorg.kotlin-conventions.gradle.kts` is compiled into a real plugin you can apply by id: ```kotlin // build-logic/src/main/kotlin/myorg.kotlin-conventions.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") // applied here, NO version } kotlin { jvmToolchain(21) } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { compilerOptions { allWarningsAsErrors.set(true) } } ``` Notice it applies `kotlin.jvm` with **no version**. For that to compile/resolve, the version must be pinned somewhere build-logic can see it. ## Where the version comes from ### Option A — marker dependency in build-logic ```kotlin // build-logic/build.gradle.kts dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24") } ``` This puts the Kotlin plugin on build-logic's classpath, so its convention plugin can apply `kotlin.jvm` by id. ### Option B — root `apply false` In the simpler, single-build variant (no separate build-logic), the **root** declares the third-party plugin `apply false` to pin the version, and convention logic (or subprojects directly) applies by id: ```kotlin // root build.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" apply false } ``` ## Consuming side Subprojects apply only your convention plugin and stay clean: ```kotlin // app/build.gradle.kts plugins { id("myorg.kotlin-conventions") } ``` ## Why this matters at scale - **One version, one config** — bumping Kotlin or changing compiler args is a single edit. - **No cross-talk** — subprojects don't repeat `apply false` or versions; they consume a curated convention. - **Configuration cache friendly** — precompiled convention plugins are typed, cacheable plugins, unlike scattered `allprojects {}` blocks. The takeaway: `apply false` is the version-pinning primitive; convention plugins are the behavior-sharing layer built on top of it.
- Inside a precompiled convention plugin you write `id('org.jetbrains.kotlin.jvm')` with no version — where does the version come from?From the convention plugin's build classpath: either the Kotlin plugin marker added as a dependency in `build-logic/build.gradle.kts`, or (in a single-build setup) the root's `apply false` versioned request. The convention plugin can only apply by id once the version is pinned upstream.
- When would you prefer build-logic + marker dependency over a root `apply false` block?When configuration (not just version) must be shared and reused, when you have many modules, or want type-safe, configuration-cache-friendly conventions. `apply false` alone only centralizes the version; it doesn't share behavior.
saying these in an interview costs you the question
- Saying a convention plugin can apply `kotlin.jvm` by id without any version pinned anywhere — it can't; the version must be on the build classpath.
- Conflating version sharing (`apply false`) with behavior sharing (convention plugins) as if one does both.