Given the plugins {} DSL is preferred, in which concrete scenarios is legacy apply() (or buildscript classpath) still necessary today?
answer
- plugins {} = restricted, current project only
- subprojects/allprojects need apply(plugin=)
- local class w/o ID -> apply<Class>()
- no marker artifact -> buildscript classpath
- conditional/dynamic -> imperative apply
basics
~20 sUse legacy apply() when applying to other projects programmatically (subprojects/allprojects), applying a local plugin class by type, applying a script plugin via apply(from=), or applying a plugin that has no marker artifact and needs a buildscript classpath.
solid answer
~50 sThe `plugins {}` DSL can't cover everything because it's restricted and declarative. Legacy apply remains necessary when: (1) applying a plugin to **other projects programmatically** — `subprojects { apply(plugin = "java") }` — since `plugins {}` only targets the script's own project and can't be put in a loop; (2) applying a **local plugin class** that has no registered ID — `apply<MyPlugin>()`; (3) applying a **script plugin** — `apply(from = "x.gradle.kts")`, which `plugins {}` doesn't do; (4) applying a plugin **published without a marker artifact** (older/internal plugins), which can only be resolved via a `buildscript { classpath(...) }` block plus `apply(plugin = "id")`; and (5) genuinely **conditional/dynamic** application that needs `if`/loops. Best practice is to push most application to `plugins {}` (with version catalogs) and convention plugins, reserving legacy apply for cross-project wiring and these edge cases.
code
kotlin · 9 lines// Marker-less internal plugin: only path is buildscript classpath + legacy apply
buildscript {
repositories { maven(url = "https://nexus.internal/repo") }
dependencies { classpath("com.acme:legacy-build-plugin:1.4.0") }
}
apply(plugin = "com.acme.legacy")
// Cross-project application stays imperative
allprojects { apply(plugin = "idea") }go deeper
Name at least the obvious case: applying to other projects with apply(plugin=) inside subprojects {}.
List several concrete cases (cross-project, local class, script plugin) and why plugins {} can't cover them.
Cover marker-less plugins and conditional application, and articulate the recommended end-state (plugins {} + convention plugins + catalogs).
Frame governance: each legacy use is an exception encapsulated in build-logic; weigh tooling, configuration cache, and maintainability impact across the org's builds.
## Why plugins {} can't do everything The `plugins {}` block is **restricted**: parsed early, no arbitrary statements, only applies to the **current** project. That design enables version resolution and type-safe accessors but rules out several legitimate needs. Legacy `apply` (imperative) fills the gaps. ## The concrete scenarios ### 1. Cross-project / programmatic application ```kotlin subprojects { apply(plugin = "java-library") apply(plugin = "jacoco") } ``` `plugins {}` only applies to `this` project and can't live inside a `subprojects {}`/`allprojects {}` loop. (Modern guidance prefers convention plugins over `subprojects {}`, but where the pattern exists, apply-by-ID is the mechanism.) ### 2. Local plugin class without an ID ```kotlin apply<com.acme.MyConventionPlugin>() ``` A `Plugin` class from `buildSrc`/included build that hasn't registered a plugin ID can't be referenced by `plugins {}`; apply it by type. ### 3. Script plugins ```kotlin apply(from = "gradle/repositories.gradle.kts") ``` `plugins {}` applies binary plugins by ID only; script-file application is exclusively `apply(from=)`. ### 4. Plugins without a marker artifact Some older or internal plugins are published as ordinary libraries with no `id.gradle.plugin` marker. `plugins {}` resolution relies on that marker, so you must: ```kotlin buildscript { repositories { maven(url = "https://internal/repo") } dependencies { classpath("com.acme:legacy-plugin:1.2.3") } } apply(plugin = "com.acme.legacy") ``` ### 5. Conditional / dynamic application ```kotlin if (providers.environmentVariable("CI").isPresent) { apply(plugin = "com.acme.ci-extras") } ``` The restricted `plugins {}` block can't contain conditionals or loops. ## The recommended end state - **Default**: `plugins {}` + `pluginManagement {}` + version catalogs for versions. - **Shared logic**: precompiled convention plugins (give them IDs, apply via `plugins {}`). - **Reserve legacy apply** for cross-project wiring, ID-less local classes, script plugins, marker-less plugins, and truly dynamic application. ## Architectural note Leaning on `buildscript`/legacy apply broadly degrades tooling, makes builds harder to reason about, and weakens configuration-cache compatibility. Treat each legacy use as a deliberate exception, ideally encapsulated inside a convention plugin so feature build scripts stay declarative.
- Why can't you put plugins {} inside a subprojects {} block?plugins {} is a restricted, declaratively-parsed block that targets only the current script's project and cannot be nested inside arbitrary configuration blocks or loops.
- What's a better alternative to subprojects { apply(...) } in modern Gradle?Precompiled convention plugins applied per-project via plugins {}, which keeps each project's configuration explicit and configuration-cache friendly.
- How do you tell if a plugin has a marker artifact?Check whether id:id.gradle.plugin:version resolves in the plugin repository; portal-published plugins have one, while some older/internal plugins published only as libraries do not.
saying these in an interview costs you the question
- Claiming plugins {} can be used inside subprojects/allprojects loops.
- Asserting every plugin can be applied via plugins {} (ignoring marker-less and script plugins).
- Treating widespread buildscript/legacy apply as fine rather than as deliberate exceptions.