What's the difference between applying a plugin by id in the `plugins {}` block versus the legacy buildscript-classpath approach that uses the real implementation coordinates directly? When would the marker indirection NOT be involved?
answer
- plugins {} = id → marker → impl
- buildscript classpath = impl GAV directly, no marker
- apply(plugin=) reads META-INF descriptor on existing classpath
- buildSrc / includeBuild / inherited = no external marker
- modern block: isolation, apply false, version catalog
basics
~20 sThe plugins {} block uses an id and resolves it through the marker module. The legacy buildscript { classpath(...) } + apply(plugin = ...) approach puts the implementation coordinate directly on the classpath, so no marker is involved — you reference the real jar by GAV.
solid answer
~50 sTwo distinct paths: - **`plugins {}` (modern):** you give an **id**. Gradle resolves it via the **marker artifact** (`<id>:<id>.gradle.plugin`) from `pluginManagement` repositories, follows the marker's dependency to the implementation jar, and applies it. Versions, capabilities, and isolated classloaders are managed for you. - **`buildscript { dependencies { classpath("group:impl:ver") } }` + `apply(plugin = "the.id")` (legacy):** you name the **implementation coordinate** directly. **No marker** is consulted — you've already placed the implementation jar on the buildscript classpath, and `apply` just looks up the id within that classpath via the `META-INF/gradle-plugins` descriptor. So the marker indirection exists **only** for the id-based `plugins {}` (and the settings `plugins {}`) resolution. Whenever you supply the implementation GAV yourself — legacy buildscript classpath, or a plugin already on the classpath from a parent/`buildSrc`/included build — the marker is bypassed. The modern block is preferred: classloader isolation, version-catalog integration, and `apply false` support.
code
kotlin · 13 lines// Modern: id resolved through the marker artifact
plugins {
id("org.springframework.boot") version "3.3.0"
}
// Legacy: implementation coordinate on the classpath — NO marker
buildscript {
repositories { mavenCentral() }
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:3.3.0")
}
}
apply(plugin = "org.springframework.boot")go deeper
Know that one path uses an id (with a marker) and the other names the real jar coordinate directly.
Contrast the two and identify that buildscript classpath skips the marker because you supply the GAV.
Explain classloader isolation, apply false, version-catalog integration, and enumerate the cases (buildSrc, includeBuild, inherited) where no marker is fetched.
Set org policy: mandate plugins {} + version catalogs, reserve buildscript classpath for unmarkered legacy plugins, and reason about classloader leakage risk.
## Two ways to get a plugin onto the build ### 1. id-based resolution (`plugins {}`) You reference a logical **id**. Because an id is not a coordinate, Gradle uses the **marker artifact** to translate id → implementation jar (see the marker mechanics). This is the modern, recommended path. It runs at script-evaluation time, supports `apply false`, integrates with version catalogs (`alias(libs.plugins.x)`), and gives each plugin request an **isolated classloader** so plugins don't leak classes into each other. ### 2. classpath-based application (legacy `buildscript`) Here you bypass ids entirely at resolution time: ```kotlin buildscript { repositories { mavenCentral() } dependencies { // the REAL implementation coordinate — not a marker classpath("org.springframework.boot:spring-boot-gradle-plugin:3.3.0") } } apply(plugin = "org.springframework.boot") // id, but resolved from the classpath above ``` The `classpath(...)` line puts the **implementation module** on the buildscript classpath directly. There is **no marker lookup** — Gradle already has the jar. The later `apply(plugin = "…")` resolves the id by scanning `META-INF/gradle-plugins/<id>.properties` on that classpath. So the id still maps to a class, but the *marker redirect* step is skipped because you supplied the coordinate yourself. ## When the marker is NOT involved - **Legacy buildscript classpath**, as above — you name the impl GAV. - **Plugin inherited from a parent project's buildscript classpath** (anti-pattern, but real). - **`buildSrc` / included build (`includeBuild`) plugins** — the plugin is built locally and on the classpath; applying it by id finds the local descriptor, no external marker fetched. - **`settings.gradle` already loaded the implementation** for a settings plugin. ## Why the modern block wins | Aspect | `plugins {}` (id + marker) | `buildscript classpath` | |---|---|---| | What you write | logical id | implementation GAV | | Marker used? | yes | no | | Classloader isolation | per-plugin, isolated | shared buildscript classpath | | `apply false` | supported | not available | | Version catalog | supported | manual | ## Practical guidance Prefer `plugins {}` everywhere. Reach for `buildscript classpath` only for legacy plugins not published with a marker, or when you must coerce a transitive version of the plugin's own dependencies before applying it.
- When you apply a buildSrc or includeBuild plugin by id, is a marker fetched from the Portal?No. The plugin is already built and on the classpath locally; applying its id resolves the local `META-INF/gradle-plugins` descriptor. No external marker is downloaded.
- Give a legitimate reason to still use buildscript classpath today.A legacy plugin published without a marker, or needing to pin/override the plugin's own transitive dependency versions before it's applied — something the isolated `plugins {}` classloader makes awkward.
- Does `apply(plugin = "x")` ever trigger marker resolution?No. `apply` only looks up an id against the already-resolved buildscript classpath; the marker redirect happens only during `plugins {}` resolution.
saying these in an interview costs you the question
- Claiming buildscript `classpath(...)` resolves a marker — it names the implementation jar directly, bypassing the marker.
- Saying buildSrc/includeBuild plugins fetch a marker from the Portal — they're local, no marker.
- Asserting `plugins {}` and buildscript share one classloader — `plugins {}` isolates per plugin.