skip to content

Gradle Kotlin DSL

build.gradle.kts is a Kotlin DSL: plugins, dependencies, and tasks blocks are receiver lambdas over typed accessors, which is why the IDE can complete them. The comparison with the dynamic Groovy DSL — compile-time safety versus flexibility — is the usual interview angle.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

6

Explain the roles of the plugins {} and dependencies {} blocks in build.gradle.kts. How do you apply a plugin and declare an implementation dependency?

level: juniorimportance: must knowfreq 65%

answer

  1. plugins = capabilities/tasks; dependencies = libraries
  2. implementation hides transitively; api exposes
  3. compileOnly vs runtimeOnly asymmetry
  4. plugins {} must come first; it generates accessors
  5. coordinate = group:artifact:version in double quotes

basics

~10 s

plugins {} applies Gradle plugins that add build capabilities and tasks. dependencies {} lists the libraries your code needs, grouped by configuration like implementation or testImplementation. You add a library with implementation("group:name:version").

solid answer

~40 s

The plugins {} block declares which Gradle plugins to apply, e.g. kotlin("jvm") version "2.0.0" or id("org.springframework.boot") version "3.x". Applying a plugin registers tasks, conventions, and — crucially in the Kotlin DSL — generates type-safe accessors (like the implementation configuration) that become available in later blocks. The dependencies {} block declares your project's libraries against a configuration: implementation(...) for normal compile+runtime deps that don't leak to consumers, api(...) to expose them transitively, testImplementation(...) for test-only deps, and runtimeOnly(...)/compileOnly(...) for the asymmetric cases. A dependency is a coordinate string "group:artifact:version", or kotlin("test") / project(":other-module") helpers. Because implementation(...) only exists after the relevant plugin (java/kotlin) is applied, ordering matters: plugins {} must precede dependencies {}.

go deeper

for a junior

Knows plugins apply build features and dependencies list libraries, and can add an implementation dependency correctly.

for a middle

Distinguishes implementation/api/compileOnly/runtimeOnly and explains plugin-before-dependency ordering.

for a senior

Explains why implementation improves incremental build performance and how api affects the consumer's compile classpath.

for a principal

Reasons about dependency hygiene and api/implementation leakage across a multi-module graph and its effect on build times.

## Two distinct jobs `plugins { }` and `dependencies { }` answer two different questions: - **`plugins { }` — "What can my build *do*?"** It applies *Gradle plugins*: reusable bundles of build logic that add tasks (`compileKotlin`, `test`, `bootJar`…), conventions, and DSL extensions. - **`dependencies { }` — "What libraries does my *code* need?"** It declares the external/internal libraries to put on the various classpaths. ## Applying plugins ```kotlin plugins { kotlin("jvm") version "2.0.0" // kotlin("x") == id("org.jetbrains.kotlin.x") id("org.springframework.boot") version "3.3.0" `java-library` // core plugin, no version, backtick-escaped id } ``` - `id("...")` references a plugin by id; `version "..."` pins it. - `kotlin("jvm")` is a Kotlin-DSL helper that expands to the JetBrains plugin id. - **Core plugins** (e.g. `` `java-library` ``, `application`) need no version. Ids containing hyphens are wrapped in backticks because of Kotlin identifier rules. Applying a plugin is what makes **type-safe accessors** appear: the Java/Kotlin plugin creates the `implementation`, `api`, `testImplementation` configurations and the DSL surfaces them as typed functions. ## Declaring dependencies ```kotlin dependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") // "group:artifact:version" api("org.slf4j:slf4j-api:2.0.13") // exposed to consumers compileOnly("org.projectlombok:lombok:1.18.32") // compile, not runtime runtimeOnly("com.h2database:h2:2.2.224") // runtime, not compile testImplementation(kotlin("test")) // helper for kotlin-test implementation(project(":shared")) // another module } ``` ## Configurations cheat-sheet - **`implementation`** — on compile + runtime classpath, **not** exposed transitively. The default choice; faster builds because changing it doesn't recompile consumers. - **`api`** — like implementation but **leaks** to consumers (only from the `java-library` plugin). Use when your public API exposes the dependency's types. - **`compileOnly`** — present at compile time only (annotation processors' APIs, provided libs). - **`runtimeOnly`** — present at runtime only (JDBC drivers, logging backends). - **`testImplementation` / `testRuntimeOnly`** — same idea, scoped to test source set. ## Ordering matters Because `implementation(...)` is generated by applying the Java/Kotlin plugin, the `plugins { }` block must come **before** `dependencies { }`. Put `plugins { }` first in the file (Gradle even requires `plugins { }` to be one of the very first statements). ## Common mistake Writing `implementation 'group:name:version'` (Groovy style) fails in `.kts`: you need parentheses and double quotes — `implementation("group:name:version")`.

  • When should you use api instead of implementation?
    Only when your module's public API returns or accepts types from that dependency, so consumers need it transitively. Otherwise implementation keeps the dependency internal and speeds up recompilation.
  • Why does plugins {} need to appear near the top of the file?
    Gradle parses it early to resolve and apply plugins before the rest of the script, and the type-safe accessors (like implementation) only exist once the plugin is applied.

saying these in an interview costs you the question

  • Confusing dependencies (libraries) with plugins (build logic)
  • Using implementation for everything when api is required, or vice versa, with no rationale
  • Putting dependencies {} before plugins {} and expecting accessors to resolve
  • Groovy-style unparenthesized single-quoted coordinates in .kts
  • Thinking compileOnly deps are available at runtime

context

open as a page

What is build.gradle.kts and how does it differ from a build.gradle file?

level: juniorimportance: must knowfreq 70%

basics

~10 s

build.gradle.kts is a Gradle build script written in Kotlin instead of Groovy. Because it is real Kotlin code, the IDE gives autocomplete, type checking, and click-through navigation that the Groovy script cannot.

open as a page

How do you define a custom task in build.gradle.kts, and what is the difference between tasks.register and tasks.create?

level: middleimportance: should knowfreq 45%

basics

~10 s

You define a task with tasks.register("name") { ... }, configuring it inside the receiver block. register creates the task lazily (only when needed), while create makes it eagerly every build, which is slower.

open as a page

What are type-safe accessors in the Gradle Kotlin DSL, where do they come from, and why might one fail to resolve?

level: middleimportance: should knowfreq 50%

basics

~10 s

Type-safe accessors are auto-generated Kotlin functions and properties that let you refer to plugin-created things (configurations, tasks, extensions) by name with full typing. Gradle generates them from the plugins you apply.

open as a page

Mechanically, how do blocks like dependencies { } work in build.gradle.kts? Explain in terms of Kotlin language features, and how this enables the type-safe DSL.

level: seniorimportance: should knowfreq 35%

basics

~20 s

Each block is a normal Kotlin function call whose last argument is a lambda with a receiver. Inside the lambda, this is set to a typed handler object, so methods like implementation(...) are really calls on that receiver.

open as a page

When advising a team, how would you weigh the Kotlin DSL against the dynamic Groovy DSL for Gradle builds? Cover correctness, performance, and migration.

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Kotlin DSL gives type safety, autocomplete, and safer refactoring; Groovy is more concise and dynamic but errors show up only at build time. For Kotlin teams the Kotlin DSL usually wins, despite slightly slower first builds and a migration cost.

open as a page