skip to content

How do precompiled convention plugins differ from legacy `apply from:` script plugins, and why are they preferred?

level: middleimportance: should knowfreq 50%

answer

  1. apply from: = uncompiled, untyped
  2. convention = compiled binary plugin
  3. type-safe accessors generated
  4. id + plugins {} block
  5. config-cache friendly

basics

~20 s

apply from: includes a raw .gradle.kts file at configuration time — uncompiled, untyped, no plugin id. Precompiled convention plugins are compiled by kotlin-dsl into binary plugins with type-safe accessors, applied via the plugins {} block by id.

solid answer

~40 s

Legacy **script plugins** are applied with `apply(from = "other.gradle.kts")`. The file is parsed and executed inline during configuration, so you get **no compilation, no type-safe accessors, no plugin id**, and poor IDE support; they also can't declare plugin dependencies in a `plugins {}` block. **Precompiled convention plugins** live in a `kotlin-dsl` project's source set; Gradle **compiles** them into binary `Plugin<Project>` implementations, generates **type-safe accessors** (so `java {}`, `kotlin {}`, custom extensions resolve with completion), assigns a **plugin id from the filename**, and lets you apply other plugins via a real `plugins {}` block. They participate in the build cache and configuration cache as compiled code. The result is faster, safer, and far more maintainable — which is why Gradle steers everyone toward convention plugins and treats `apply from:` as legacy.

code

kotlin · 7 lines
kotlin
// LEGACY: no id, no type-safe accessors
apply(from = "../gradle/java.gradle.kts")

// MODERN: compiled, typed, id-addressable
plugins {
    id("com.acme.java-conventions")
}

go deeper

for a junior

Know that apply from: is the old way and convention plugins are the recommended replacement.

for a middle

List the concrete differences: compilation, type-safe accessors, plugin id, plugins {} block.

for a senior

Discuss configuration-cache and maintainability implications and when migrating legacy builds.

for a principal

Define a migration strategy off apply from: across many repos and the standards that make convention plugins the default.

## Two ways to factor out shared build logic ### 1. Legacy script plugins — `apply from:` ```kotlin // build.gradle.kts apply(from = "../gradle/quality.gradle.kts") ``` Gradle reads `quality.gradle.kts` and executes it against the current `Project` at **configuration time**. Characteristics: - **Not compiled ahead of time** — re-evaluated each run, no separate compilation unit. - **No type-safe accessors** — inside the script you often can't use `java { }` blocks for plugins applied elsewhere; you fall back to `configure<JavaPluginExtension> { }` or `extensions.getByType(...)`. - **No plugin id**, so it can't appear in a `plugins { }` block of consumers. - **No `plugins { }` block of its own** — it can only `apply(plugin = ...)` imperatively, which loses type-safe accessors for those plugins. - **Weak IDE support** — limited completion and navigation. ### 2. Precompiled convention plugins Put the same logic in `build-logic/src/main/kotlin/com.acme.quality.gradle.kts` in a project that applies `kotlin-dsl`. Now: - Gradle **compiles** it into a binary `Plugin<Project>` class. - It gets a **plugin id** = filename (`com.acme.quality`). - It can use a **real `plugins { }` block**, and the `kotlin-dsl` plugin **generates type-safe accessors** so `java { }`, `checkstyle { }`, your own extension, etc. are all available with IDE completion *inside the convention plugin*. - Consumers apply it the idiomatic way: ```kotlin plugins { id("com.acme.quality") } ``` ## Why precompiled wins | Aspect | apply from: | Precompiled convention | |---|---|---| | Compiled | No | Yes | | Type-safe accessors | No | Yes | | Plugin id / `plugins {}` apply | No | Yes | | Own `plugins {}` block | No | Yes | | IDE completion | Poor | Full | | Config-cache friendliness | Weaker | Strong | ## When script plugins still linger You may still see `apply from:` for tiny one-off includes or in very old builds. Gradle's guidance is to migrate to convention plugins. The only real cost of convention plugins is the up-front `build-logic`/`buildSrc` scaffolding, which pays for itself the moment more than one module shares logic.

  • Why can't a legacy `apply from:` script use a `plugins {}` block?
    The `plugins {}` block is only valid at the top of a real build/settings script or a precompiled script plugin. An applied-from script runs as an inline fragment, so it must use imperative `apply(plugin = ...)` instead, losing type-safe accessors.
  • What gives a precompiled convention plugin its type-safe accessors?
    The `kotlin-dsl` plugin generates accessors for the extensions/configurations contributed by any plugins requested in the convention plugin's own `plugins {}` block, so `java {}`, the test task, and custom extensions resolve with completion.

saying these in an interview costs you the question

  • Saying `apply from:` and convention plugins are interchangeable.
  • Claiming script plugins have plugin ids.
  • Thinking type-safe accessors work automatically inside an `apply from:` script.

context