skip to content

Precompiled Script Convention Plugins

Authoring convention plugins as .gradle.kts files in a build-logic project, where the filename becomes the plugin id. Interviewers ask because this is the sanctioned way to share configuration across subprojects today.

on this pageshow

questions

5

What is a precompiled script convention plugin in Gradle, and what problem does it solve?

level: juniorimportance: must knowfreq 70%

answer

  1. *.gradle.kts in src/main/kotlin
  2. kotlin-dsl plugin compiles them
  3. plugin id = filename
  4. replaces subprojects {} duplication
  5. applied by id in plugins {}

basics

~10 s

It's a *.gradle.kts file placed in a build-logic project's source set that Gradle compiles into a plugin. You apply it by id to share common build configuration across many subprojects instead of copy-pasting it.

solid answer

~40 s

A precompiled script convention plugin is a regular Gradle build script (`something.gradle.kts` or `.gradle`) placed under `src/main/kotlin` (or `groovy`) of a project that applies the `kotlin-dsl` plugin — usually a `build-logic` included build or `buildSrc`. Gradle compiles each such file into a binary `Plugin<Project>` whose **plugin id is derived from the filename** (`com.acme.java-conventions.gradle.kts` → id `com.acme.java-conventions`). Subprojects then `apply` it by that id in their `plugins { }` block. This solves the DRY problem in multi-project builds: instead of duplicating Java/Kotlin toolchain setup, repository declarations, common dependencies, and test configuration in every subproject, you centralize it in one convention plugin. It is the modern, type-safe replacement for cross-project `allprojects {}`/`subprojects {}` configuration.

code

kotlin · 27 lines
kotlin
// build-logic/src/main/kotlin/com.acme.java-conventions.gradle.kts
plugins {
    `java-library`
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}

// any-module/build.gradle.kts
plugins {
    id("com.acme.java-conventions")
}

go deeper

for a junior

Recall that it's a *.gradle.kts file you apply by id to avoid copy-pasting build config.

for a middle

Explain the kotlin-dsl compilation step, the filename→id rule, and that it replaces subprojects {}.

for a senior

Contrast with binary Plugin<Project> classes, discuss included build wiring, and the project-isolation motivation.

for a principal

Frame convention plugins as the org-wide standard for build governance — one source of truth for toolchains, quality gates, and dependency baselines across many repos.

## The problem: configuration duplication In a multi-project build with many modules, each `build.gradle.kts` often needs the same setup — the Java toolchain, the `repositories {}` block, shared test dependencies, JUnit platform wiring, code-quality plugins. Copy-pasting that into every module is unmaintainable, and the old workaround — `subprojects {}` / `allprojects {}` in the root build — is discouraged because it couples projects together and breaks configuration-on-demand and project isolation. ## What a precompiled convention plugin is A **precompiled script plugin** is an ordinary Gradle build script — written in the same DSL you already know — that lives in the *source set* of a project rather than being applied as a build script directly. You put it under: ``` build-logic/ settings.gradle.kts src/main/kotlin/ com.acme.java-conventions.gradle.kts ``` The enclosing project must apply the **`kotlin-dsl`** plugin (or `groovy-gradle-plugin` for Groovy). `kotlin-dsl` scans `src/main/kotlin` for `*.gradle.kts` files and **compiles each one into a binary `Plugin<Project>`** at build time. So a script you'd normally write inline becomes a reusable, statically-compiled plugin. ## Plugin id from filename The generated plugin's **id is the filename minus the `.gradle.kts` suffix**. So `com.acme.java-conventions.gradle.kts` registers a plugin with id `com.acme.java-conventions`. If you put the file in a package directory and add a `package` declaration, the package becomes a *namespace* prefix is NOT added automatically — the id is still just the filename; the package only affects the generated class. (A leading filename segment with dots, e.g. `com.acme.foo`, becomes a dotted id directly.) ## How it's wired into the main build The `build-logic` directory is typically an **included build**: the root `settings.gradle.kts` calls `includeBuild("build-logic")` inside `pluginManagement {}`. That makes the compiled convention plugins available to every project's `plugins {}` block by id. (`buildSrc` is the zero-config alternative — its conventions are visible automatically — but the buildSrc-vs-build-logic placement tradeoff is a separate topic.) ## Applying it ```kotlin // some-module/build.gradle.kts plugins { id("com.acme.java-conventions") } ``` That's it — the module inherits the toolchain, repositories, and dependencies defined once in the convention plugin. ## Why precompiled rather than a hand-written Plugin<Project> Because you write **plain build-script DSL** — `java { }`, `dependencies { }`, `tasks.test { }` — with full type-safe accessors and IDE completion, instead of the more verbose imperative `project.extensions.getByType(...)` API you'd use in a binary `Plugin<Project>` class. It's the most ergonomic on-ramp to plugin authoring.

  • What is the plugin id of a file named `com.acme.kotlin-library.gradle.kts`?
    `com.acme.kotlin-library` — the id is the filename with the `.gradle.kts` extension stripped.
  • Which plugin must the build-logic project apply for this to work?
    `kotlin-dsl` (for Kotlin scripts) or `groovy-gradle-plugin` (for Groovy). It compiles the `*.gradle.kts`/`*.gradle` files into binary plugins and provides the type-safe accessors.

Like extracting a repeated block of code into a shared function: write the build logic once, call it by name everywhere.

saying these in an interview costs you the question

  • Claiming the plugin id must be declared manually — it is derived from the filename.
  • Saying you write a `Plugin<Project>` class — precompiled scripts ARE the script, no class needed.
  • Confusing it with `apply from:` script-plugin includes (those are not compiled or type-safe).

context

open as a page

How do you apply and configure other plugins inside a precompiled convention plugin, and how do you get type-safe accessors for them?

level: middleimportance: must knowfreq 55%

basics

~20 s

Request them in the convention plugin's own plugins {} block. The kotlin-dsl plugin then generates type-safe accessors so you can configure them with java { }, kotlin { }, etc. Their plugin coordinates must be on the build-logic project's classpath.

open as a page

What are the rules and gotchas around file placement, packages, and ids for precompiled convention plugins?

level: middleimportance: should knowfreq 35%

basics

~20 s

Place *.gradle.kts files in src/main/kotlin of a kotlin-dsl project. The plugin id is the filename minus .gradle.kts. A package declaration namespaces the generated class but does not change the id; the dotted filename itself forms the id.

open as a page

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

level: middleimportance: should knowfreq 50%

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.

open as a page

How would you design a layered set of convention plugins for a large multi-module build, and what pitfalls would you avoid?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Create small, single-responsibility convention plugins (base, library, application, test, publishing) and compose them by applying lower-layer ones inside higher-layer ones. Avoid one monolithic plugin and avoid leaking module-specific logic into shared conventions.

open as a page