skip to content

In a multi-module Gradle build, why do teams declare `alias(libs.plugins.kotlin.jvm) apply false` in the root project, and what error occurs if a subproject re-declares the version?

level: seniorimportance: should knowfreq 45%

answer

  1. apply false = on classpath, not applied here
  2. Pin version once in root + catalog
  3. Subprojects apply with no version
  4. One id = one version across the plugin graph
  5. Re-declaring version => 'already requested at a different version'

basics

~20 s

Declaring a plugin once in the root with apply false puts it on the build's classpath at a single version without activating it there. Subprojects then apply it version-free, so every module uses the same version and you avoid version conflicts.

solid answer

~40 s

`apply false` declares a plugin in the `plugins {}` block so its implementation is added to the build's plugin classpath, but it is **not applied** to that project. In a root `build.gradle.kts` you list all community plugins once (`alias(libs.plugins.kotlin.jvm) apply false`), pinning one version build-wide. Subprojects then write `alias(libs.plugins.kotlin.jvm)` (no version) to actually apply it. If two places try to define a version — e.g. a subproject writes `id("org.jetbrains.kotlin.jvm") version "..."` while the root already pinned it — Gradle fails with a 'plugin request ... was already requested at a different version' / 'Error resolving plugin' conflict, because a plugin id may only be requested at one version across the build's plugin graph. The catalog + `apply false` pattern guarantees a single version and keeps subproject scripts minimal.

code

kotlin · 12 lines
kotlin
// root build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm) apply false
}

// subprojects/api/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.jvm)   // no version: inherited from catalog
}

// WRONG in a subproject -> build error:
// plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" }

go deeper

for a junior

Recognizes apply false means the plugin is declared but not applied here.

for a middle

Explains pinning the version once in root + catalog and applying version-free in subprojects.

for a senior

Diagnoses the 'already requested at a different version' conflict and knows the one-id-one-version rule across the plugin graph.

for a principal

Designs the build so convention plugins + catalog enforce a single version org-wide and keep subproject scripts declarative and cache-friendly.

## The problem: version drift across modules In a multi-module build, every module that uses the Kotlin plugin must use the **same version** — Gradle resolves a single classpath per plugin id across the build's plugin graph. If module A requests Kotlin 2.0.20 and module B requests 1.9.24, Gradle reports a conflict; you cannot load two versions of the same plugin id. ## `apply false` — declare without applying In the `plugins {}` block, `apply false` means: *resolve and add this plugin to the build's classpath, but do not apply it to the current project.* ```kotlin // root build.gradle.kts plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false } ``` The root project itself usually has no source, so it does not want Kotlin compilation applied — it only wants to **pin the version centrally**. Each subproject then applies the plugin with no version: ```kotlin // module-a/build.gradle.kts plugins { alias(libs.plugins.kotlin.jvm) // version comes from the root/catalog } ``` Because the version is already fixed (by the catalog's `version.ref` and the root declaration), subprojects must **not** restate it. ## The conflict if you restate the version If a subproject writes: ```kotlin plugins { id("org.jetbrains.kotlin.jvm") version "1.9.24" // BAD: re-declares version } ``` Gradle fails the build with an error like *“The request for this plugin could not be satisfied … plugin was already requested at a different version.”* Even restating the **same** version where it is already managed can raise *“Plugin request for plugin already on the classpath must not include a version.”* The rule: declare the version **once** (root + catalog), apply **without** a version everywhere else. ## Why this scales - **Single source of truth:** bump `kotlin` in `libs.versions.toml` once; all modules move together. - **Thin subproject scripts:** they only list which plugins to apply. - **Configuration-cache friendly:** the plugin graph is stable and declarative. ## Convention-plugin alternative Large builds push shared `plugins {}` + config into a **convention plugin** (a precompiled script plugin in `buildSrc` or an included build). Subprojects then apply `id("myproject.kotlin-conventions")`, and the convention plugin internally applies the Kotlin plugin — still relying on the catalog (`libs`) for the version.

  • Does `apply false` add the plugin's tasks to the root project?
    No. It only puts the plugin's implementation on the classpath at a fixed version; tasks and extensions appear only in projects that actually apply it (no `apply false`).
  • How would a convention plugin change this setup?
    You move the shared `plugins {}` and configuration into a precompiled script plugin (buildSrc/included build); subprojects apply that one convention id, and it applies Kotlin internally, still drawing the version from the catalog.

saying these in an interview costs you the question

  • Restating the version in every subproject and not understanding the resulting conflict
  • Thinking `apply false` applies the plugin but suppresses its tasks
  • Believing two versions of the same plugin id can coexist in one build
  • Putting the Kotlin plugin version inline instead of in the catalog
  • Not knowing the root project is typically source-free and only pins versions

context