skip to content

What kind of logic is forbidden inside the plugins {} block, and how does that differ from the imperative apply() approach?

level: middleimportance: should knowfreq 58%

answer

  1. declarative mini-language
  2. no if/for/val in plugins {}
  3. apply() = imperative, conditional ok
  4. apply loses type-safe accessors
  5. convention plugin = idiomatic

basics

~20 s

The plugins {} block accepts only the constrained plugins DSL — id(...), version, apply false. No conditionals, loops, variables, or arbitrary method calls. Imperative apply(plugin = "...") is normal Kotlin code, so it can be wrapped in if blocks or loops.

solid answer

~40 s

Inside `plugins {}` you may only use the restricted plugins DSL: `id("...")`, an optional `version "..."`, and `apply false`. You **cannot** use `if`/`when`, `for` loops, local `val`s, string interpolation for the ID, or call arbitrary functions — the block is statically extracted, so it must be analyzable without running general code. The legacy imperative form, `apply(plugin = "...")` (Kotlin) or `apply plugin: '...'` (Groovy), is ordinary script code executed in the body, so you can apply plugins conditionally, in a loop, or based on a computed value. The trade-off: imperative `apply` loses the early-resolution and type-safe accessors of `plugins {}`. The idiomatic answer is to keep `plugins {}` declarative and push conditional/dynamic plugin logic into a **convention (precompiled script) plugin** or settings logic instead of reaching for `apply`.

code

kotlin · 9 lines
kotlin
// Declarative — required form inside plugins {}
plugins {
    id("org.springframework.boot") version "3.2.0" apply false
}

// Conditional application must use imperative apply in the body:
if (project.hasProperty("useBoot")) {
    apply(plugin = "org.springframework.boot")
}

go deeper

for a junior

Know plugins {} is declarative-only and that apply() is the older imperative alternative.

for a middle

List what's forbidden (if/for/val/dynamic id) and the trade-offs of falling back to apply().

for a senior

Recommend convention plugins to keep scripts declarative while supporting reuse/conditionality.

for a principal

Define org standards: ban ad-hoc apply() in favor of governed convention plugins and version catalogs.

## The constraint Because Gradle statically extracts the `plugins {}` block before running the script, the block is a **declarative mini-language**, not general Kotlin/Groovy. Allowed: - `id("plugin.id")` - `id("plugin.id") version "1.2.3"` - `id("plugin.id") apply false` - the Kotlin-DSL type-safe accessors (`java`, `application`, …) - `version` references from a version catalog (`alias(libs.plugins.foo)`) Forbidden: - `if` / `when` to apply a plugin conditionally - `for` loops over a list of IDs - declaring `val`s or computing the ID string at runtime - calling arbitrary helper functions ```kotlin plugins { // ILLEGAL — won't compile: if (project.hasProperty("useBoot")) { id("org.springframework.boot") version "3.2.0" } } ``` ## The imperative escape hatch The older `apply` mechanism runs as ordinary script code in the body: ```kotlin if (project.hasProperty("useBoot")) { apply(plugin = "org.springframework.boot") } ``` This works, but: - The plugin is applied **later**, during script execution, not in the early pass. - You lose **type-safe accessors** — you must reference the plugin's extensions/tasks via string names or generic APIs. - The plugin's artifact must already be on the **buildscript classpath** (via `buildscript {}` or a parent), because `apply` doesn't resolve from the Plugin Portal. ## The idiomatic resolution When you genuinely need conditional or shared plugin application, the modern answer is a **convention plugin** — a precompiled script plugin in `buildSrc` or an included build — that itself uses `plugins {}` and encapsulates the logic, then is applied declaratively by each module. This keeps build scripts declarative while still allowing reusable, parameterized behavior. ## Summary table | Aspect | `plugins {}` | imperative `apply()` | |---|---|---| | Logic allowed | declarative only | full Kotlin/Groovy | | When applied | early static pass | during script body | | Type-safe accessors | yes | no | | Resolves from Portal | yes | no (needs classpath) |

  • If you need to conditionally apply a plugin, what's the cleanest approach?
    Encapsulate it in a convention plugin (buildSrc / included build) and apply that declaratively, or — if truly dynamic — use imperative apply() in the body, accepting the loss of type-safe accessors.
  • Why can't you interpolate a variable into the id string inside plugins {}?
    Because the block is statically extracted before any code runs, so the ID must be a compile-time constant string literal.
  • Does apply() resolve a plugin from the Gradle Plugin Portal?
    No — apply() only applies a plugin already on the buildscript classpath; you'd need buildscript {} dependencies to get the artifact there.

saying these in an interview costs you the question

  • Claiming you can wrap id(...) in an if/for inside plugins {}.
  • Forgetting that imperative apply() loses type-safe accessors and doesn't resolve from the Portal.

context