skip to content

In a Kotlin/Groovy build script, what is the implicit 'this' and how do unqualified calls like dependencies {}, repositories {}, or tasks resolve?

level: middleimportance: should knowfreq 50%

answer

  1. this == Project
  2. unqualified = project.member
  3. extensions add DSL blocks
  4. settings → Settings, init → Gradle
  5. Kotlin typed receiver vs Groovy delegate

basics

~10 s

Inside a build.gradle(.kts), the implicit receiver ('this') is the Project object. So unqualified calls like dependencies {}, repositories {}, tasks, group, and version are actually members of Project (e.g. project.dependencies { }).

solid answer

~40 s

A build script is compiled with the `Project` as its **delegate / implicit receiver**. Every top-level statement is implicitly `project.<member>`. So `dependencies {}` is `project.dependencies {}`, `repositories {}` is `project.repositories {}`, `tasks` is `project.tasks`, and setting `group`/`version` sets `project.group`/`project.version`. In Kotlin DSL the script class implements the `Project` interface as its receiver, giving typed accessors and IDE completion; in Groovy it works via Groovy's `delegate` and dynamic dispatch. Plugins **extend** this surface: the `java` plugin adds the `java {}` extension and `sourceSets`, so after `plugins { id("java") }` you can call `java { ... }` unqualified — it resolves to `project.extensions.getByType<JavaPluginExtension>()`. Understanding the Project delegate explains why the same name can be a method (`dependencies {}`) or property (`dependencies` returning the handler), and why extensions/conventions added by plugins appear as new top-level DSL.

code

kotlin · 6 lines
kotlin
// All resolve against the Project receiver:
group = "com.katajob"
version = "1.0.0"
repositories { mavenCentral() }      // project.repositories
dependencies { implementation("org.springframework:spring-core") }
tasks.named("test") { useJUnitPlatform() }  // project.tasks

go deeper

for a junior

Know that 'this' is the Project and dependencies/repositories are project methods.

for a middle

Explain extensions adding new DSL and the different delegate for settings/init scripts.

for a senior

Discuss Kotlin typed accessors generated from applied plugins vs Groovy dynamic delegate, and migration implications.

for a principal

Frame DSL-surface governance: convention plugins exposing custom extensions, typed accessor stability, and standardizing Kotlin DSL org-wide for compile-time safety.

## The implicit receiver Gradle compiles each `build.gradle(.kts)` into a class whose **implicit receiver** (Kotlin) or **delegate** (Groovy) is the project's `Project` instance. That means any unqualified name is resolved against `Project` first. ```kotlin // these two are identical: dependencies { implementation("...") } project.dependencies { implementation("...") } group = "com.example" // project.group = ... version = "1.0.0" // project.version = ... val libsDir = layout.projectDirectory // project.layout... ``` ## Common Project members you call unqualified - `dependencies { }` → `DependencyHandler` - `repositories { }` → `RepositoryHandler` - `tasks` → `TaskContainer` (`tasks.register`, `tasks.named`) - `configurations` → `ConfigurationContainer` - `extensions`, `plugins`, `layout`, `providers`, `objects` - properties: `group`, `version`, `description`, `buildDir`/`layout.buildDirectory` ## How plugins add to the surface Plugins register **extensions** on `project.extensions`. The `java` plugin registers a `JavaPluginExtension` under the name `java`, which the Kotlin DSL exposes as a typed accessor: ```kotlin java { // project.extensions.getByType<JavaPluginExtension>() toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } ``` Because the accessor is generated from the **applied** plugins, this is another reason `plugins {}` must be evaluated early — the typed `java {}` accessor only exists once the plugin is applied. Without the plugin, you'd have to use `extensions.configure<...>` or it wouldn't resolve. ## settings.gradle is different The **settings** script's delegate is `Settings`, not `Project` — that's why `include(...)`, `rootProject`, `pluginManagement {}`, and `dependencyResolutionManagement {}` live there. Init scripts delegate to `Gradle`. Knowing the delegate per script type tells you which API is in scope. ## Kotlin vs Groovy nuance In **Kotlin DSL** the receiver is statically typed (`Project`), so unresolved names are compile errors and you get completion. In **Groovy** resolution is dynamic via the metaclass `delegate`, so typos surface at runtime. This is a major reason teams migrate to Kotlin DSL for safety.

  • Why does the java {} block only resolve after applying the java plugin?
    The plugin registers a JavaPluginExtension on project.extensions; the Kotlin DSL's typed `java {}` accessor is generated from applied plugins, so without the plugin there is no such extension to configure.
  • What is the delegate of settings.gradle.kts and why does include() live there?
    Its delegate is the `Settings` object, which owns project composition; `include`, `pluginManagement`, and `dependencyResolutionManagement` are Settings members, not Project members.

saying these in an interview costs you the question

  • Saying the build script delegate is Settings (that's settings.gradle)
  • Claiming java {} works without applying any plugin

context