skip to content

DSL Extensions & Conventions

Creating your own build-script syntax with extensions.create() and ExtensionAware, and the the<>()/configure<>{} access it produces. Interviewers ask because it explains where blocks like java { } or kotlin { } come from, and why the old Convention mechanism was deprecated.

on this pageshow

questions

5

What is a Gradle extension, and how does adding one give a plugin its own configuration block in the build script?

level: juniorimportance: must knowfreq 70%

answer

  1. extensions.create(name, Type)
  2. Project is ExtensionAware
  3. name in script == registered name
  4. ExtensionContainer holds instances
  5. replaces Convention

basics

~10 s

An extension is an object a plugin registers via project.extensions.create("name", Type::class.java). Gradle then exposes a build-script block named after it (e.g. name { ... }) that configures that object.

solid answer

~40 s

A Gradle **extension** is a plain object a plugin attaches to the project so users can configure the plugin. The plugin calls `project.extensions.create("greeting", GreetingExtension::class.java)`. Because `Project` is `ExtensionAware`, Gradle registers the object under that name in the project's `ExtensionContainer` and, crucially, generates a build-script DSL block: a method/property named `greeting` that takes a configuration closure/Action. So in the script you write `greeting { message = "hi" }`, which is just calling the configuration action against that extension instance. The extension holds the settings; the plugin's tasks read them later (ideally lazily via `Property`). This is the standard, non-deprecated way to add DSL — it replaced the old `Convention` mechanism. Extensions are also `ExtensionAware` themselves, which is what lets you nest blocks.

code

kotlin · 14 lines
kotlin
abstract class GreetingExtension {
    abstract val message: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val ext = project.extensions.create("greeting", GreetingExtension::class.java)
        project.tasks.register("hello") {
            doLast { println(ext.message.get()) }
        }
    }
}
// build.gradle.kts:
// greeting { message = "hi" }

go deeper

for a junior

Know that extensions.create(name, Type) adds a configuration block named 'name' and that Project is ExtensionAware.

for a middle

Explain the ExtensionContainer, that the registered string is the DSL name, and that the plugin reads values from the kept reference (lazily).

for a senior

Discuss why typed extensions beat ad-hoc properties (completion, contract), lazy reads, and that this replaces Convention.

for a principal

Frame extensions as the stable public API surface of a plugin and the governance implications of naming/backward compatibility.

## What an extension is A Gradle **extension** is an ordinary JVM object that a plugin registers on the project to expose user-facing configuration. It is the bridge between *what the user writes in the build script* and *what the plugin's tasks read*. ## ExtensionAware and the ExtensionContainer Every `Project` implements `ExtensionAware`, which exposes an `ExtensionContainer` via `project.extensions`. Plugins add objects to it: ```kotlin project.extensions.create("greeting", GreetingExtension::class.java) ``` The first argument is the **public name**. That name becomes the build-script identifier. When Gradle compiles the script it sees `greeting { ... }` and resolves it to a dynamically-added method that runs the closure/`Action` against the registered `GreetingExtension` instance. ## How the DSL block appears There is no special syntax in the script for "this is an extension". `greeting { ... }` is the same shape as any other Gradle config block; Gradle's dynamic-object machinery routes it to the extension you registered under that name. That is why the *registered name* — not the class name — is what users type. ```kotlin // plugin abstract class GreetingExtension { abstract val message: Property<String> } class GreetingPlugin : Plugin<Project> { override fun apply(project: Project) { val ext = project.extensions.create("greeting", GreetingExtension::class.java) project.tasks.register("hello") { doLast { println(ext.message.get()) } } } } ``` ```kotlin // build.gradle.kts consuming it greeting { message = "Hello from the extension" } ``` ## Why a class, not just variables Using a typed extension object gives users IDE completion, type safety, and a clear public contract. The plugin keeps a reference to the instance so its tasks can read the values during execution. Modeling those values as `Property<T>` (lazy) lets users set them anywhere in the script regardless of evaluation order. ## Relationship to Convention Before extensions, plugins mixed configuration into a `Convention` object. That API is deprecated/removed in modern Gradle; `extensions.create()` is the supported replacement.

  • What determines the name of the DSL block users type in the build script?
    The first argument to extensions.create() — the registered name in the ExtensionContainer — not the extension class's name.
  • Why does the plugin keep the reference returned by create()?
    So its tasks can read the configured values later. Reading via Property defers the read to execution time and avoids ordering problems.

The extension is a settings form the plugin pins to the project; the build-script block is just the place where the user fills that form in.

saying these in an interview costs you the question

  • Thinking the block name comes from the class name rather than the registered string.
  • Claiming extensions are still configured through Convention.
  • Reading extension values eagerly during apply() instead of lazily at execution.

context

open as a page

From a build script or another plugin, how do you access and configure an extension that some other plugin registered? Contrast the<>() and configure<>{}.

level: middleimportance: must knowfreq 55%

basics

~10 s

Use the Kotlin DSL helpers: the<MyExtension>() returns the extension instance to read, and configure<MyExtension> { ... } runs a configuration block against it. Both look it up by type in the ExtensionContainer.

open as a page

How do you give an extension nested configuration blocks (a sub-block inside the extension's block) in modern Gradle?

level: middleimportance: should knowfreq 40%

basics

~10 s

Either expose a @Nested property that Gradle materializes (managed nested object), or, because an extension is itself ExtensionAware, register a child extension on it via extension.extensions.create(...) so it gets its own nested DSL block.

open as a page

What was the Gradle Convention mechanism, why was it deprecated, and how do you migrate plugin DSL off it?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Convention was the old way plugins added properties/methods to the project (project.convention.plugins). It was untyped and error-prone, so Gradle deprecated it (removed in Gradle 9). Migrate by replacing convention objects with extensions.create().

open as a page

What is the difference between extensions.create() and extensions.add(), and when would you choose one over the other?

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

create() instantiates the extension for you via ObjectFactory (decorated, with managed Property/@Nested support) and registers it. add() registers an instance you already built yourself, with no decoration. Prefer create().

open as a page