skip to content

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?

level: seniorimportance: should knowfreq 35%

answer

  1. plugins {} = id → marker → impl
  2. buildscript classpath = impl GAV directly, no marker
  3. apply(plugin=) reads META-INF descriptor on existing classpath
  4. buildSrc / includeBuild / inherited = no external marker
  5. modern block: isolation, apply false, version catalog

basics

~20 s

The 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 s

Two 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
kotlin
// 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

for a junior

Know that one path uses an id (with a marker) and the other names the real jar coordinate directly.

for a middle

Contrast the two and identify that buildscript classpath skips the marker because you supply the GAV.

for a senior

Explain classloader isolation, apply false, version-catalog integration, and enumerate the cases (buildSrc, includeBuild, inherited) where no marker is fetched.

for a principal

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.

context