skip to content

A teammate adds `id('org.jetbrains.kotlin.jvm') version '1.9.24'` (with the version) to two different subprojects' build scripts and the build fails. Why, and how does the `apply false` pattern fix it?

level: middleimportance: must knowfreq 50%

answer

  1. version requested once per shared classpath
  2. duplicate explicit request = error even if equal
  3. one versioned request at root
  4. subprojects: id only, no version
  5. single source of truth

basics

~20 s

Requesting the same plugin version in two projects sharing a classpath conflicts. You fix it by requesting the version once at the root with apply false, then applying by id (no version) in each subproject.

solid answer

~40 s

Subprojects in a build share a single plugin/build-script classpath managed via the `plugins {}` mechanism. A plugin version can be **requested with a version only once** on that shared classpath. When two sibling subprojects each write `version '1.9.24'`, Gradle reports a conflict like 'plugin already on the classpath with a different/duplicate version request' — even if the numbers match, the duplicate explicit request is the problem. The `apply false` pattern resolves this: you make exactly one versioned request, in the **root** build script, with `apply false` so the root doesn't activate the plugin. Every subproject then writes `id('org.jetbrains.kotlin.jvm')` with no version and applies cleanly, inheriting the pinned version. This is the canonical single-source-of-truth setup and is what version-catalog plugin aliases do under the hood.

code

kotlin · 13 lines
kotlin
// FAILS: two subprojects each request the version
// module-a/build.gradle.kts
plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" }
// module-b/build.gradle.kts
plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" }  // conflict

// FIX: one versioned request at root with apply false
// root build.gradle.kts
plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" apply false }
// module-a/build.gradle.kts
plugins { id("org.jetbrains.kotlin.jvm") }
// module-b/build.gradle.kts
plugins { id("org.jetbrains.kotlin.jvm") }

go deeper

for a junior

Recognize the symptom (duplicate version request) and that the fix is declaring the version once.

for a middle

Explain the shared-classpath single-version-request rule and write the correct root apply false + subproject id-only layout.

for a senior

Relate it to version catalogs and convention plugins, and explain when a subproject legitimately needs a different version (separate buildscript classpath).

for a principal

Discuss governance: enforcing one pinned version across many modules/repos, renovate/dependabot bumps at the catalog, and avoiding drift at scale.

## The shared-classpath rule When you use the `plugins {}` DSL, plugin resolution happens against a classpath that is effectively shared across the projects in the build. Gradle's contract is: **a plugin's version may be declared (requested with `version`) at most once** for that classpath. The point is determinism — Gradle must put exactly one version of the plugin's classes on the classpath, so it refuses ambiguity rather than silently picking one. If two subprojects both write `version '1.9.24'`, you get an error along the lines of *"Error resolving plugin … plugin request … already on the classpath with version …"*. The duplicate explicit request is illegal even when the versions are identical. ## How `apply false` fixes it Move the single versioned request up to the root and disable application there: ```kotlin // root build.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" apply false } ``` Now there is **one** versioned request in the whole build. Each subproject applies without a version: ```kotlin // module-a/build.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") } // module-b/build.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") } ``` Both apply cleanly, both get 1.9.24, and bumping the version is a one-line change at the root. ## Why `apply false` and not just apply it at the root If you dropped `apply false`, the root would apply the Kotlin plugin and gain a Kotlin source set / compile tasks it doesn't want. `apply false` keeps the root purely as the version-pinning point. ## Relationship to allprojects/subprojects blocks A legacy alternative is a `subprojects { apply(plugin = "…") }` block, but that uses the older `apply()` mechanism and the buildscript classpath must already carry the plugin. The modern, recommended shape is root `plugins { … apply false }` + per-subproject `plugins { id("…") }`, which keeps plugin requests declarative and lets Gradle reason about them.

  • The two versions were identical — why does Gradle still complain?
    The rule is about the number of explicit versioned *requests* on the shared classpath, not whether the numbers agree. Two explicit requests are ambiguous to Gradle's plugin resolution, so it errors regardless of equality.
  • Where does a version catalog put this single request?
    In `libs.versions.toml` under `[plugins]`; the root then writes `alias(libs.plugins.kotlin.jvm) apply false` and subprojects `alias(libs.plugins.kotlin.jvm)` — same one-request-at-root shape, version centralized in the catalog.

saying these in an interview costs you the question

  • Claiming identical versions in two subprojects are fine — the duplicate explicit request still errors.
  • Proposing to fix it by applying the plugin at the root without `apply false`, which pollutes the root project.

context