skip to content

In a large multi-module build, how would you structure plugin version resolution so versions are declared once and applied consistently across modules?

level: seniorimportance: should knowfreq 45%

answer

  1. one source of truth
  2. catalog + root apply false
  3. convention plugins in build-logic
  4. pluginManagement.plugins alternative
  5. publish catalog for org-wide policy

basics

~20 s

Declare every plugin version once in gradle/libs.versions.toml under [plugins]. Resolve them at the root with alias(...) apply false (or use convention plugins), then apply by alias with no version in each module. This keeps versions DRY and conflict-free.

solid answer

~40 s

Pick a single source of truth and reference it everywhere. The cleanest approach is a **version catalog**: declare all plugin coordinates under `[plugins]` in `gradle/libs.versions.toml`. At the **root** build script, list the shared plugins with `alias(libs.plugins.x) apply false` to resolve (seed) their versions on the build classpath without applying them. Each module then applies by alias with no version. For larger codebases, go further with **convention plugins** in `build-logic` (an included build): each convention plugin applies a curated set of plugins (still versioned via the catalog, which you make accessible to `build-logic`), and modules just apply your convention plugin id. This removes duplication, prevents version-conflict errors, and gives one place to bump a version. Avoid re-declaring versions in module scripts — that invites drift and conflicts.

code

kotlin · 14 lines
kotlin
// settings.gradle.kts — share build-logic + catalog
pluginManagement { includeBuild("build-logic") }

// root build.gradle.kts — resolve shared plugin versions once
plugins {
    alias(libs.plugins.kotlinJvm) apply false
    alias(libs.plugins.springBoot) apply false
}

// module build.gradle.kts — apply convention OR alias, no literal version
plugins {
    id("myorg.kotlin-conventions")
    alias(libs.plugins.springBoot)
}

go deeper

for a junior

Beyond scope; at most recall 'put versions in the catalog'.

for a middle

Describe catalog + root apply false and apply-by-alias in modules.

for a senior

Compare catalog vs convention-plugins vs pluginManagement and justify a choice with trade-offs.

for a principal

Treat the catalog as published org-wide policy; design CI enforcement and upgrade workflow across repos.

## The problem In a 50-module build, repeating `id("org.springframework.boot") version "3.3.0"` in every script means 50 edits to bump a version and a real risk of two modules disagreeing (a **plugin version conflict**). The goal: declare each version **once**, apply **consistently**. ## Strategy A — version catalog + root `apply false` ```toml # gradle/libs.versions.toml [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } ``` ```kotlin // root build.gradle.kts — resolve once, don't apply here plugins { alias(libs.plugins.springBoot) apply false alias(libs.plugins.kotlinJvm) apply false } // module build.gradle.kts — apply, no version plugins { alias(libs.plugins.kotlinJvm) alias(libs.plugins.springBoot) } ``` The catalog holds versions; the root seeds them; modules reference by alias. One file to edit on upgrade. ## Strategy B — convention plugins (build-logic) For cross-cutting setup (compiler args, test config, *and* a fixed set of plugins), put **precompiled script plugins** in an included build: ```kotlin // build-logic/src/main/kotlin/myorg.kotlin-conventions.gradle.kts plugins { id("org.jetbrains.kotlin.jvm") } // ...shared config... ``` ```kotlin // settings.gradle.kts pluginManagement { includeBuild("build-logic") } // module build.gradle.kts plugins { id("myorg.kotlin-conventions") } ``` Now modules apply **one** id and inherit a vetted plugin set. To version the plugins the convention applies, expose the catalog to `build-logic` (e.g. via `versionCatalogs` in its settings) so the convention plugin's own dependencies pull versions from the same TOML. ## Strategy C — pluginManagement.plugins {} in settings A lighter alternative: pin versions centrally in `settings.gradle.kts` under `pluginManagement { plugins { id("x") version "y" } }`, then modules apply `id("x")` version-less. Good when you don't want a catalog but still want one place for plugin versions. ## Trade-offs | Approach | Centralizes versions | Centralizes config | Complexity | |---|---|---|---| | Catalog + root apply false | yes | no | low | | Convention plugins | yes (via catalog) | yes | medium | | pluginManagement.plugins | yes | no | low | ## Conflict avoidance Whatever you pick, the rule is the same: **one** declaration of each plugin's version. Mixing a catalog alias in one module and a literal `version` in another for the same id risks a conflict. CI can lint for stray literal versions. ## Governance angle A catalog can be **published** and shared across repositories (`settings.gradle.kts` → `versionCatalogs { create("libs") { from("com.myorg:catalog:1.0") } }`), turning version policy into an org-wide, reviewable artifact.

  • Why use `apply false` at the root instead of just applying the plugins there?
    The root usually isn't a Kotlin/Spring module itself; `apply false` resolves the version onto the classpath so children can apply it version-less, without forcing the plugin's behavior onto the root project.
  • How do you make catalog plugin versions available inside build-logic convention plugins?
    Expose the catalog to the included build (e.g. declare/import the same `libs.versions.toml` in build-logic's settings) so the convention plugin's dependencies use `version.ref` from the shared catalog.
  • How can you enforce that nobody re-declares a literal plugin version in a module?
    Add a CI check or a custom Gradle/lint rule scanning build scripts for `version "..."` inside plugins blocks, failing the build if a non-catalog literal appears.

saying these in an interview costs you the question

  • Recommending repeating literal versions in every module.
  • Saying convention plugins automatically supply plugin versions without wiring the catalog into build-logic.
  • Ignoring conflict risk when mixing catalog aliases and literal versions for the same id.

context