skip to content

What is the legacy apply(plugin = "...") syntax in Gradle, and how does it differ from the modern plugins {} DSL?

level: juniorimportance: must knowfreq 60%

answer

  1. imperative method call vs declarative block
  2. no version resolution on its own
  3. needs buildscript classpath
  4. type-safe accessors only with plugins {}
  5. still needed for apply<Class> / subprojects / from=

basics

~20 s

apply(plugin = "...") is the old imperative way to apply a plugin by ID anywhere in the build script. The modern plugins {} DSL is a declarative block at the top that Gradle understands ahead of time.

solid answer

~40 s

`apply(plugin = "java")` is the legacy, imperative way to apply a plugin: it runs as a normal method call during script execution, so it can appear anywhere and even be conditional. The modern `plugins {}` block is **declarative** — Gradle parses it before executing the script body, which lets it resolve plugin versions from the plugin marker repository, give type-safe accessors, and apply the plugin to the correct target. The legacy form has no version resolution of its own (the plugin must already be on the buildscript classpath), provides no extension type-safety until applied, and can't be statically analyzed. You still see `apply` for applying a plugin from a precompiled `Plugin` class, applying to subprojects programmatically, or `apply(from = ...)` for script plugins.

code

kotlin · 13 lines
kotlin
// Legacy imperative
buildscript {
    repositories { mavenCentral() }
    dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:3.3.0") }
}
apply(plugin = "java")
apply(plugin = "org.springframework.boot")

// Modern declarative equivalent
plugins {
    java
    id("org.springframework.boot") version "3.3.0"
}

go deeper

for a junior

Know that apply() is the old way and plugins {} is the modern declarative way, and that plugins {} goes at the top.

for a middle

Explain the declarative-early-parsing advantage: version resolution and type-safe accessors, plus why legacy needs the buildscript classpath.

for a senior

Articulate the precise cases where legacy apply is still required (apply<Class>, cross-project, apply(from=)) and the consequences for tooling/statically analyzed builds.

for a principal

Frame a migration policy: standardize on plugins {} + version catalogs, reserve legacy apply for convention plugins/subproject wiring, and explain how this affects build cacheability and tooling.

## Two ways to apply a plugin A Gradle **plugin** is a reusable unit of build logic (tasks, configurations, extensions). Before you can use what a plugin contributes, you must **apply** it to a target — usually a `Project`. ### Legacy imperative apply `apply(plugin = "id")` is a regular method on the `PluginAware`/`Project` object. Because it's an ordinary method call, it executes top-to-bottom during the configuration of the script and can be wrapped in `if` blocks, loops, or called against other projects: ```kotlin apply(plugin = "java") apply(plugin = "org.springframework.boot") ``` The catch: legacy `apply` does **not** resolve the plugin from the Gradle Plugin Portal by itself. The plugin's implementation class must already be on the **buildscript classpath**, which is why legacy usage almost always pairs with a `buildscript { dependencies { classpath(...) } }` block. ### Modern declarative plugins {} DSL ```kotlin plugins { java id("org.springframework.boot") version "3.3.0" } ``` Gradle reads the `plugins {}` block **before** executing the rest of the script. This early, restricted evaluation lets Gradle: - resolve the plugin's **marker artifact** from the configured plugin repositories using the declared `version`, - generate **type-safe accessors** (e.g. the `java {}` extension, task accessors) for the rest of the script, - apply the plugin in a well-defined order. ### When legacy apply is still required - Applying a plugin **class** you wrote in `buildSrc` or an included build: `apply<MyPlugin>()`. - Applying a plugin to **another project** programmatically (e.g. inside `subprojects { }` or `allprojects { }`): `apply(plugin = "java")`. - Applying a **script plugin**: `apply(from = "gradle/quality.gradle.kts")`. - Some plugins published only as ordinary libraries (no marker artifact) still need the `buildscript` classpath + legacy apply. ### Key contrast | | `apply(plugin=)` | `plugins {}` | |---|---|---| | Evaluation | imperative, anytime | declarative, parsed early | | Version resolution | none (needs classpath) | yes, from plugin repos | | Type-safe accessors | no | yes | | Conditional/loops | yes | no |

  • Why does legacy apply usually require a buildscript {} block?
    Because legacy apply has no version resolution; the plugin's implementation must already be on the buildscript classpath, which the buildscript dependencies { classpath(...) } block provides.
  • Can you put plugins {} inside an if statement?
    No. The plugins {} block is restricted and parsed before script execution, so it can't contain arbitrary conditional logic. Legacy apply() can be made conditional.

saying these in an interview costs you the question

  • Claiming apply(plugin=) resolves the plugin version from the Plugin Portal like plugins {} does.
  • Saying the two forms are interchangeable in every situation (they're not for version resolution and type-safe accessors).

context