skip to content

Extension & Convention Model

How a plugin exposes configuration to build scripts: extensions, nested DSL objects, convention defaults, and wiring those values into tasks. Interviewers ask because it is the difference between a usable plugin and a pile of tasks.

on this pageshow

explore

questions

27

What does Property.convention(...) do on a Gradle lazy property, and how does it differ from calling set(...)?

level: juniorimportance: must knowfreq 62%

answer

  1. convention = default, set = override
  2. explicit always wins
  3. get() throws if neither set
  4. convention can be a Provider (lazy)
  5. no-op if set already happened

basics

~10 s

convention(...) supplies a default value used only if nobody calls set(...). set(...) is an explicit assignment that overrides any convention. So convention = fallback default, set = the actual chosen value.

solid answer

~40 s

On a `Property<T>` (or `Provider`-backed type), `convention(value)` registers a *default* that is returned by `get()` only when no explicit value has been assigned. `set(value)` (or Kotlin `=`) assigns the real value and always wins over the convention. The key rule: **convention does not override an explicit set, and a later convention call does not override an earlier set**. This lets a plugin author declare sensible defaults in the plugin while still letting the build script override them. Conventions can themselves be lazy — `convention(provider { ... })` defers computation. Internally the property tracks whether it is 'explicitly set'; if not, reads fall through to the convention. This is the modern replacement for eagerly initialising fields, and it keeps configuration lazy so values resolve at execution time.

code

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

val ext = extensions.create<GreetingExtension>("greeting")
ext.message.convention("Hello")   // default
// build author may do: greeting { message = "Hi" }
println(ext.message.get())        // "Hi" if set, else "Hello"

go deeper

for a junior

State plainly: convention is the default, set is the override, and set wins. Mention get() can throw when nothing is provided.

for a middle

Add that conventions can be lazy via convention(provider{...}) and explain the resolution order (explicit → convention → undefined).

for a senior

Discuss why plugins prefer convention over set for composability with disallowChanges/finalizeValueOnRead and detecting whether the author configured a value.

for a principal

Frame it as part of the lazy-configuration contract that enables configuration cache and clean plugin/consumer separation of defaults vs overrides.

## What a lazy property is Gradle's configuration model uses *lazy properties*: `Property<T>`, `ListProperty<T>`, `MapProperty<T>`, `RegularFileProperty`, `DirectoryProperty`, and the read-only `Provider<T>`. Instead of holding a value directly, they hold a *recipe* that is evaluated on `get()`. This defers work until it is actually needed and lets values be wired together before any of them are known. ## `convention` vs `set` A property has two distinct slots: - the **convention** — a default supplied via `convention(value)` or `convention(provider)`; - the **explicit value** — assigned via `set(value)` (Groovy/Java) or `=` (Kotlin assignment), or `value(...)`. The resolution rule on `get()`: 1. If an explicit value was assigned, return it. 2. Otherwise, if a convention was registered, return the convention's value. 3. Otherwise the property is *undefined* — `get()` throws, while `getOrNull()` returns null and `getOrElse(d)` returns `d`. Two subtle but exam-critical rules: - **A convention never overrides an explicit value.** Once something has called `set(...)`, calling `convention(...)` afterwards has no visible effect. - **Calling `convention(...)` after `set(...)` is a no-op for reads**, and calling `set(...)` after `convention(...)` overrides the convention. Order of the two *kinds* of call doesn't change the precedence — explicit always wins. ## Why use `convention` instead of just `set` in a plugin If a plugin did `ext.outputDir.set(layout.buildDirectory.dir("reports"))`, that would be an *explicit* value, and a build author's own `set(...)` would still win — but so would order-of-evaluation surprises, and you lose the ability to detect 'was this ever configured'. Using `convention(...)` declares intent: 'this is my default unless overridden'. It keeps the property *unset* from the build author's perspective, which composes correctly with `disallowChanges()`, `finalizeValueOnRead()`, and downstream wiring. ## Laziness of the convention itself `convention(provider { expensiveDefault() })` means the default is only computed if the convention is actually read (i.e., nobody set the property). Combined with `layout.buildDirectory`, this is the idiomatic way to default file/dir properties. ```kotlin abstract class GreetingExtension { abstract val message: Property<String> abstract val outputDir: DirectoryProperty } project.extensions.create<GreetingExtension>("greeting").apply { message.convention("Hello from Gradle") outputDir.convention(layout.buildDirectory.dir("greetings")) } ``` Here the build can override `greeting { message = "Hi" }`, and the convention silently steps aside.

  • What happens if you call convention("a") after the build already called set("b")?
    Nothing visible — get() still returns "b". An explicit value always wins over a convention regardless of call order.
  • What does get() do if neither set nor convention was called?
    It throws an exception (the property has no value). Use getOrNull() or getOrElse(default) to read safely.

convention is like a factory default setting; set(...) is the user changing it in the menu. Once the user changes it, restoring the factory default text on the box doesn't un-change their choice.

saying these in an interview costs you the question

  • Saying convention(...) overrides set(...) — it's the opposite.
  • Claiming a later convention() replaces an earlier set() — explicit always wins.
  • Confusing convention with a guaranteed value: get() can still throw if nothing was provided and there's no convention.

context

open as a page

What is a Gradle DSL extension, and how do you register one from a plugin so users can configure your plugin in their build script?

level: juniorimportance: must knowfreq 70%

basics

~10 s

An extension is an object added to a project that exposes a configuration block (DSL). A plugin registers it with project.extensions.create("myExt", MyExtension::class.java), then users configure it via a myExt { ... } block.

open as a page

What is a NamedDomainObjectContainer in Gradle, and why would a plugin author expose one in their extension?

level: juniorimportance: must knowfreq 55%

basics

~10 s

It's a Gradle collection of objects each identified by a unique name. Plugins expose one so users can declare multiple named instances in a DSL block, like sourceSets or configurations.

open as a page

Your plugin defines an extension with a `Property<String>` called `greeting`. How do you connect that extension value to a task's input property so the task uses whatever the user sets in the build script?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Use task.greeting.set(extension.greeting). This wires the task's Property to the extension's Property lazily, so the value is read later (at execution), not while configuring.

open as a page

Why are modern Gradle extensions written as abstract classes with abstract Property<> / ListProperty<> getters, and who provides the implementations?

level: middleimportance: must knowfreq 60%

basics

~20 s

You declare the extension abstract with abstract Property/ListProperty getters and no bodies. Because Gradle instantiates the class via ObjectFactory, it generates a subclass that implements those getters with initialized, lazy property objects — you don't write boilerplate.

open as a page

In a NamedDomainObjectContainer, what is the difference between register(name), create(name), and maybeCreate(name)?

level: middleimportance: must knowfreq 50%

basics

~10 s

create makes the element eagerly now. register defers creation until needed (configuration avoidance) and returns a provider. maybeCreate returns the existing element of that name or eagerly creates it if absent.

open as a page

How do you give a Gradle extension a nested configuration block (a sub-extension) that users configure with a block in the build script, like `myExtension { server { host = ... } }`?

level: middleimportance: must knowfreq 55%

basics

~10 s

Add a property of the nested type to the extension, then add a method that takes an Action<NestedType> and calls action.execute(nested). Gradle's DSL lets users configure it with a block.

open as a page

When building a nested extension, why do you create the nested object with ObjectFactory.newInstance and declare its fields as abstract Property getters instead of plain fields with a constructor?

level: middleimportance: must knowfreq 50%

basics

~10 s

ObjectFactory.newInstance makes Gradle create a managed object: it injects services and materializes the abstract Property getters for you, keeping values lazy and wirable into tasks. A plain constructor gives none of that.

open as a page

A teammate's plugin reads `extension.outputDir.get()` inside the plugin's `apply` method to configure a task, and reports that the value users set in the build script is being ignored. Diagnose the bug and fix it.

level: middleimportance: must knowfreq 55%

basics

~10 s

.get() resolves the value during configuration, before the user's DSL block runs, so it captures the default. Fix it by wiring the provider lazily: task.outputDir.set(extension.outputDir) with no .get().

open as a page

A plugin reads myExt.message.get() inside its apply() method and the user's configured value is always missing. What's wrong and how do you fix it while still creating the extension correctly?

level: seniorimportance: must knowfreq 45%

basics

~20 s

The extension is created in apply(), but the user's myExt { } block runs later as the script evaluates. Reading .get() in apply() observes the value before configuration. Fix it by consuming the Property lazily — pass the provider through instead of calling get() eagerly.

open as a page

Walk through authoring a custom extensible named-collection DSL with objects.domainObjectContainer: what does the element type need, how do you expose it, and what makes the nested block work?

level: seniorimportance: must knowfreq 35%

basics

~20 s

Define an abstract element type with a name constructor param and Property-typed fields, create the container with objects.domainObjectContainer(Type), add it as an extension, and Gradle auto-generates the myThings { foo { } } nested DSL keyed by name.

open as a page

Gradle had a legacy Convention object (project.convention / the Convention API). How does it relate to the modern extension/convention-property model, and what replaced it?

level: middleimportance: should knowfreq 28%

basics

~10 s

The old Convention object let plugins mix extra properties/methods into the DSL (convention objects/plugins). It's deprecated and removed in Gradle 9. The replacement is the ExtensionContainer (project.extensions.create) plus lazy Property.convention(...) defaults.

open as a page

What do finalizeValue(), finalizeValueOnRead(), and disallowChanges() do on a property, and when would you use each when designing an extension?

level: middleimportance: should knowfreq 40%

basics

~10 s

They lock a property. disallowChanges() forbids further set() calls. finalizeValue() resolves the current value now and freezes it. finalizeValueOnRead() defers that freeze until the first get(). All prevent later mutation of configured values.

open as a page

Explain the difference between extensions.create(name, type) and extensions.create(name, type, constructorArgs...). When would you pass constructor arguments?

level: middleimportance: should knowfreq 40%

basics

~20 s

Both register a named extension that Gradle instantiates. The overload with extra args passes them to the extension's constructor (after any injected services). You use it when the extension needs values not available via injection, like a reference to the owning project or a child container.

open as a page

After registering an extension, how do you retrieve the same instance elsewhere, and what's the difference between getByType, findByName, and capturing the create() return value?

level: middleimportance: should knowfreq 35%

basics

~10 s

extensions.create returns the instance, so capture it directly. Later you can fetch it from the ExtensionContainer: getByType(MyExtension::class.java) (typed, throws if absent) or findByName("myExt") (by name, returns null/Object if absent).

open as a page

How does configureEach differ from all{} and forEach on a NamedDomainObjectContainer, and why does the distinction matter for configuration avoidance?

level: middleimportance: should knowfreq 42%

basics

~10 s

configureEach registers a lazy action that runs per element only when each is realized, so it doesn't force creation. all{} and forEach are eager — they realize every element immediately, defeating configuration avoidance.

open as a page

What is a NamedDomainObjectProvider, how do you obtain one, and how does it keep container access lazy?

level: middleimportance: should knowfreq 38%

basics

~10 s

It's a lazy handle to a named element returned by register or named(name). Holding or configuring it (configure { }) doesn't realize the element; only get() does — so wiring stays deferred.

open as a page

What does the `@Nested` annotation do in Gradle, and where do you apply it?

level: middleimportance: should knowfreq 45%

basics

~10 s

@Nested marks a getter whose returned object holds further input/output-annotated properties. Gradle walks into that object and treats its annotated members as part of the task's inputs for up-to-date and caching checks.

open as a page

How do you wire collection-typed and file-system extension values (e.g. `ListProperty<String>`, `DirectoryProperty`) into a task, and what wiring methods differ from a plain `Property`?

level: middleimportance: should knowfreq 40%

basics

~10 s

Wire them the same lazy way with set: task.tags.set(extension.tags) for ListProperty, task.outDir.set(extension.outDir) for DirectoryProperty. Collections also offer add/addAll to append lazily instead of replacing.

open as a page

How do you set a convention default that depends on another property or on the build layout, and what pitfalls come from eager vs lazy convention values?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use the provider overload: convention(provider { ... }) or convention(otherProperty.map { ... }). It stays lazy, so the default is computed at read time and can depend on values configured later. Passing an already-computed value captures it eagerly.

open as a page

When would you use an ExtensiblePolymorphicDomainObjectContainer, and how does registerFactory / registerBinding enable a polymorphic named DSL?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Use it when one named container must hold several distinct element types (like tasks). You register a factory or type binding per subtype, then users create typed elements by name with register(name, Type), getting a polymorphic DSL.

open as a page

Gradle objects like Project and tasks are ExtensionAware. How do you add your own nested extension to another extension to build a richer, deeply-nested DSL?

level: seniorimportance: should knowfreq 30%

basics

~10 s

If a type implements ExtensionAware, you call theObject.extensions.create("name", Type::class.java) to attach a sub-extension to it. Making your own extension ExtensionAware lets third parties nest their own blocks under yours.

open as a page

When designing a plugin DSL, when should you model configuration as a nested sub-extension versus flat properties on the top-level extension? What are the trade-offs?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Use nesting to group cohesive, related settings (one feature area) into a typed block for discoverability and reuse. Keep things flat when there are only a few settings or no clear grouping. Don't over-nest — deep DSLs are hard to navigate.

open as a page

Why is wiring extension providers into tasks (rather than capturing values via closures or eager reads) essential for configuration-cache compatibility and reliable up-to-date checks?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Provider wiring keeps task inputs as serializable, declared lazy values resolved at execution. Closures over the project/extension capture live objects the configuration cache can't serialize, and eager reads bypass input tracking, breaking up-to-date checks.

open as a page

Inside a plugin, how can you tell whether a build author explicitly configured an extension property versus it still holding only the convention default — and why does the convention mechanism enable that?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Because a convention is a separate slot from the explicit value, the property stays 'unset' until someone calls set(). A plugin can branch on that by reading the property without a convention, or by comparing against the known default, to detect author configuration.

open as a page

How does Gradle treat a `@Nested` collection (e.g. a `List` of bean specs) for up-to-date checks, and why does element identity matter?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Gradle fingerprints every element of a @Nested collection by reading each element's input annotations. Element identity (list index, or Named name) determines how changes — including reordering — affect the task's fingerprint and cache key.

open as a page

When wiring extension values into tasks across projects or when an extension value depends on another task's output, how do you keep the wiring lazy and correct — and what does `finalizeValueOnRead` add?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Keep wiring providers, including ones backed by task outputs — task.input.set(otherTask.flatMap { it.output }) — so Gradle infers task dependencies automatically. finalizeValueOnRead locks a property's value on first read to catch late mutations and avoid inconsistent reads.

open as a page