skip to content

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

level: seniorimportance: should knowfreq 35%

answer

  1. convention.plugins[name] = obj
  2. flat shared namespace → collisions
  3. deprecated 7.1, removed Gradle 9
  4. replace with extensions.create()
  5. getPlugin → getByType

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().

solid answer

~40 s

**Convention** was Gradle's original extensibility mechanism: a plugin registered a convention object via `project.convention.plugins["name"] = obj`, and its properties/methods were dynamically mixed into the project's namespace so build scripts could call them directly. It predates extensions and had real problems: name collisions across plugins, no clear ownership, weak typing, and confusing resolution order with extensions. Gradle deprecated `Convention`/`HasConvention` (warnings since 7.1, **removed in Gradle 9**). The migration is to register the same configuration object as an **extension**: `project.extensions.create("name", MyExt::class.java)` instead of a convention. Where the old code read a convention via `project.convention.getPlugin(Type::class.java)`, switch to `extensions.getByType(Type::class.java)` / `the<Type>()`. Built-in plugins did the same migration (e.g. the Java plugin's source-set and base conventions moved to extensions). The net effect: typed, name-scoped, IDE-friendly DSL with no shared mutable namespace.

code

kotlin · 9 lines
kotlin
// Migration: Convention -> Extension
// OLD (removed in Gradle 9):
// project.convention.plugins["greeting"] = GreetingConvention(project)
// val c = project.convention.getPlugin(GreetingConvention::class.java)

// NEW:
val ext = project.extensions.create("greeting", GreetingExtension::class.java)
val read = project.extensions.getByType(GreetingExtension::class.java)
// or: the<GreetingExtension>()

go deeper

for a junior

Awareness that Convention is old/deprecated and extensions are the modern way is sufficient.

for a middle

Explain what Convention did (shared namespace) and that you migrate by registering an extension instead.

for a senior

Drive a concrete migration: replace convention.plugins/getPlugin with extensions.create/getByType, including built-in conventions, and cite Gradle 9 removal.

for a principal

Plan org-wide plugin migrations off Convention ahead of Gradle 9 upgrades and prevent new code from reintroducing it.

## What Convention was In early Gradle, plugins extended the build model through **conventions**. A plugin created a convention object and registered it: ```groovy project.convention.plugins['java'] = new JavaPluginConvention(project) ``` Gradle then **merged that object's properties and methods into the project's dynamic namespace**, so a script could write `sourceCompatibility = ...` as if it were a project property. `Convention` was reachable through `project.convention` (`HasConvention`). ## Why it was deprecated 1. **Flat shared namespace** — every plugin's convention properties lived in one bag on the project, so two plugins could collide, and it was unclear which plugin owned a given property. 2. **Weak typing / discoverability** — dynamic mixing meant little IDE help and surprising resolution between conventions and extensions. 3. **Redundant with extensions** — `ExtensionAware`/`extensions.create()` already provided named, typed, scoped configuration objects, making Convention obsolete. Gradle deprecated the `Convention` and `HasConvention` types (deprecation warnings from 7.1 onward) and **removed them in Gradle 9.0**. Plugins still using `project.convention` break on Gradle 9. ## How to migrate Replace the convention with an extension of the same shape. ```kotlin // before (deprecated): // project.convention.plugins["greeting"] = GreetingConvention() // read: project.convention.getPlugin(GreetingConvention::class.java) // after: val ext = project.extensions.create("greeting", GreetingExtension::class.java) // read elsewhere: project.extensions.getByType(GreetingExtension::class.java) // or the<GreetingExtension>() ``` Steps: 1. Turn the convention class into an extension type (prefer abstract + `Property` fields). 2. Swap `convention.plugins[name] = obj` for `extensions.create(name, Type)`. 3. Replace every `convention.getPlugin(...)` read with `extensions.getByType(...)`. 4. For built-in conventions you consumed (e.g. `JavaPluginConvention.sourceSets`), use the modern extension (`SourceSetContainer` via `the<SourceSetContainer>()` / `extensions.getByType`), as those built-ins migrated too. ## Why the new model is better Extensions are **named and scoped** (no global mixing), **typed** (compile/IDE checks), and **composable** (each extension is `ExtensionAware`, enabling nesting). The DSL syntax users see (`greeting { ... }`) is identical or cleaner, so migration is mostly internal.

  • In which Gradle version were Convention and HasConvention actually removed?
    Gradle 9.0. They were deprecated with warnings starting around 7.1; plugins relying on project.convention fail on 9.x.
  • If you previously read project.convention.getPlugin(SomeConvention), what replaces it now?
    extensions.getByType(SomeType::class.java) (or the<SomeType>() in Kotlin DSL) against the extension that replaced the convention.
  • Does migrating change the build-script syntax users see?
    Usually not — greeting { ... } looks the same; the change is internal (typed, named extension instead of a shared convention namespace).

saying these in an interview costs you the question

  • Claiming Convention is still the recommended way to add DSL.
  • Saying extensions and conventions are interchangeable today — Convention is removed in Gradle 9.
  • Confusing the deprecated Convention API with the unrelated concept of Property.convention() default values.

context