When are script plugins still an appropriate choice, and when should you reach for buildSrc/build-logic or a published binary plugin instead?
answer
- trivial+local → script plugin
- non-trivial+single repo → build-logic
- cross-repo → published binary plugin
- buildSrc/build-logic = compiled + tested + ID
- one ID > many apply(from=) copies
basics
~10 sUse script plugins for tiny, build-local, low-ceremony sharing (version constants, one shared task). Move to buildSrc/build-logic precompiled plugins for compiled, testable, type-safe convention logic, and to a published binary plugin when sharing across repositories.
solid answer
~50 sScript plugins (`apply(from = ...)`) are a **tactical convenience**: zero setup, just a file. They're appropriate when the shared logic is **small, build-local, and not order-sensitive** — e.g. a `versions.gradle.kts` of constants, or applying one shared task to a few modules. They stop scaling once you need: **type-safe accessors**, **compilation and unit tests**, a **stable plugin ID**, IDE navigation, or sharing across **multiple repositories**. At that point, **precompiled script plugins** in `buildSrc` or a `build-logic` **included build** (via `includeBuild`) give you compiled convention plugins with IDs, applied through `plugins {}`. For cross-repository reuse, **publish a binary plugin** to a repository (Plugin Portal or internal Maven). The decision axis is roughly: trivial+local → script plugin; non-trivial+single-repo → build-logic precompiled plugin; shared across repos → published binary plugin. As a system-design rule, standardize conventions in one place so every module applies one ID rather than copy-pasting `apply(from=)` lines.
code
kotlin · 9 lines// settings.gradle.kts — graduate to build-logic
includeBuild("build-logic")
// build-logic/src/main/kotlin/my.java-conventions.gradle.kts
plugins { java }
java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } }
// consumer build.gradle.kts — one ID instead of apply(from=)
plugins { id("my.java-conventions") }go deeper
Know script plugins are for small local sharing and buildSrc/build-logic are for bigger reusable logic.
Map the tiers: script plugin vs buildSrc/build-logic vs published plugin, with the accessor/test/ID trade-offs.
Drive the decision by repo scope and ordering/testing needs; justify includeBuild over buildSrc.
Establish org build-conventions governance: one versioned published plugin as source of truth, script plugins reserved for trivial local snippets.
## A decision ladder **Tier 0 — Script plugin (`apply(from=)`)** Use when: logic is a few lines, build-local, low risk, and you don't need accessors/tests/ID. Examples: shared version constants via `extra`, a single diagnostic task, a tiny repository block reused by 2-3 modules. Pros: zero ceremony. Cons: no accessors, no compilation/tests, positional ordering pitfalls, weak discoverability, supply-chain risk if loaded by URL. **Tier 1 — Precompiled convention plugin in `buildSrc`** File: `buildSrc/src/main/kotlin/my.java-conventions.gradle.kts` with `kotlin-dsl` applied. Compiled to a `Plugin<Project>` with ID `my.java-conventions`, applied via `plugins { id("my.java-conventions") }`. You regain type-safe accessors and can write tests. Downside: changes to `buildSrc` invalidate the whole build's configuration cache more aggressively. **Tier 2 — `build-logic` included build** Same as Tier 1 but in a separate included build wired with `includeBuild("build-logic")` in settings. Better isolation than `buildSrc`, can host multiple plugins, and is the modern recommended pattern for non-trivial convention logic. **Tier 3 — Published binary plugin** When many repositories need the same conventions, publish the plugin (to the Gradle Plugin Portal or an internal Maven repo) and consume it by ID + version through `pluginManagement`. This gives versioning, changelogs, and true cross-repo governance. ## Choosing ```text Trivial + single build -> script plugin (apply(from=)) Non-trivial + single repository -> build-logic precompiled plugin (includeBuild) Shared across many repositories -> published binary plugin (id + version) ``` ## Why not just script plugins everywhere? - **No type safety / accessors** → fragile `configure<>()`/string lookups. - **No compilation or tests** → logic can't be validated in isolation. - **Positional evaluation** → ordering heisenbugs. - **No versioning** → can't roll conventions out gradually across repos. ## System-design framing The goal is **one source of truth** for build conventions. A single `plugins { id("my.java-conventions") }` per module beats N copies of `apply(from = "../conventions.gradle.kts")`, and a published plugin lets a platform team evolve conventions with versioned, reviewable releases.
- Why prefer a build-logic included build over buildSrc for convention plugins today?build-logic gives stronger isolation, can host multiple plugins, and changes interact more gracefully with the configuration cache than buildSrc, whose changes tend to invalidate more of the build.
- What does publishing a binary plugin add over a build-logic plugin?Versioning and cross-repository distribution: consumers pin a version via pluginManagement, enabling gradual, reviewable rollout of convention changes across many repos.
saying these in an interview costs you the question
- Recommending script plugins for cross-repository standardization.
- Treating buildSrc/build-logic and apply(from=) as interchangeable.
- Ignoring testing/type-safety/versioning trade-offs in the choice.