skip to content

DSL Examples & Idioms

The recognizable DSL shapes in the wild: markup builders, Gradle build scripts, routing and configuration blocks, and operator-based composition. Interviewers often start from one of these and ask how it works underneath.

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

explore

questions

21

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

In a kotlinx.html-style DSL like html { body { p { +"hi" } } }, what Kotlin language feature makes the nested blocks work, and why can each block call functions like body or p without a qualifier?

level: juniorimportance: must knowfreq 60%

basics

~10 s

Each block is a lambda that runs 'on' an object (a receiver). Inside the block you can call that object's functions directly, so body() and p() are methods of the current builder.

open as a page

What is an infix function in Kotlin, and what are the rules for declaring one? Use it to explain how `1 to "a"` works.

level: juniorimportance: must knowfreq 70%

basics

~20 s

An infix function lets you call it without a dot or parentheses, like 1 to "a". You mark it with the infix keyword. It must be a member or extension function with exactly one parameter and no default value.

open as a page

Explain how a config DSL like Json { isLenient = true } works under the hood. What two Kotlin language features make this `{ ... }` block possible?

level: juniorimportance: must knowfreq 75%

basics

~10 s

The braces are a function passed as the last argument. Inside, this is a settings object, so you set properties like isLenient directly without naming the object.

open as a page

In the markup DSL the text inside a tag is written as +"hello". What operator is this, how do you implement it, and why is it used instead of a plain function call?

level: middleimportance: must knowfreq 55%

basics

~20 s

The plus sign is the unary-plus operator. You define operator fun String.unaryPlus() on the tag class so it adds the string as a text child. It's used because it reads cleanly and only works inside a tag.

open as a page

How does operator overloading work in Kotlin? Name the function-name conventions for `+`, `[]` get/set, `in`, and `..`, and explain what `operator` does.

level: middleimportance: must knowfreq 60%

basics

~20 s

Kotlin maps symbols to specially named functions. + calls plus, a[i] calls get, a[i]=v calls set, x in c calls contains, a..b calls rangeTo. You mark each with the operator keyword so the compiler allows the symbolic call.

open as a page

In Ktor, how does `routing { route("/api") { get("/users") { } } }` build a nested route tree? Describe the receiver types at each level and why nesting works.

level: middleimportance: must knowfreq 70%

basics

~10 s

Each block is a lambda whose this is a route node. route("/api") makes a child node and runs the inner lambda against it, so get inside attaches to /api, building a tree of routes.

open as a page

Without @DslMarker, what bug can appear in a nested markup DSL, and how does annotating tag types with a @DslMarker annotation fix it?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Without it, an inner block can accidentally call functions from an outer tag, building a wrong tree. @DslMarker tells the compiler to hide outer receivers inside nested blocks, so only the closest tag's functions are available implicitly.

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

Sketch a minimal type-safe HTML builder so that html { body { p { +"Hello" } } } compiles and can render to a string. Identify the receiver lambdas, the child-attachment step, and the unaryPlus.

level: middleimportance: should knowfreq 40%

basics

~20 s

Make a Tag class holding children. Each tag function creates a child, adds it to children, runs the block on it, and returns it. Add operator fun String.unaryPlus() to append text. A render() walks children to build the HTML string.

open as a page

Compare Ktor's `install(ContentNegotiation) { json() }` configuration DSL with a settings-object DSL like `Json { }`. How are their builder lambdas structured, and what does the configuration lambda's receiver type tell you?

level: middleimportance: should knowfreq 40%

basics

~20 s

Both use a trailing lambda over a settings object. Json { } configures a JsonBuilder; install(Plugin) { } configures that plugin's own Configuration class. The receiver type tells you which settings are available inside the block.

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

You're designing a tiny assertion DSL with `infix fun <T> T.shouldBe(expected: T)`. Walk through how the infix call resolves, generics, nullability, and one limitation you'd warn the team about.

level: seniorimportance: should knowfreq 35%

basics

~20 s

actual shouldBe expected calls actual.shouldBe(expected). The generic T is inferred from both sides, so types must line up. You should warn that infix doesn't chain cleanly and equality uses equals, which can surprise with platform types or floating-point.

open as a page

Explain the `unaryPlus` and `invoke` conventions and how they make builder-style DSLs read naturally (e.g. `+"text"` inside an HTML builder, or `dependencies { implementation(...) }`).

level: seniorimportance: should knowfreq 45%

basics

~20 s

unaryPlus lets +x mean 'add x', so +"hello" inside a builder appends text. invoke lets you call an object like a function — thing(args) runs thing.invoke(args). Both remove method-name noise so DSL blocks read cleanly.

open as a page

You are designing a config DSL `httpClient { timeout = 30; retry { maxAttempts = 3 } }` over a settings object. How do you structure the builder so configuration is validated once and the resulting config is immutable? Walk through the design.

level: seniorimportance: should knowfreq 35%

basics

~10 s

Use a mutable builder with var fields and nested builder functions. The top-level function creates the builder, runs the user's lambda, validates, then copies the values into an immutable data class it returns.

open as a page

When building a nested config/routing DSL, why might inner blocks accidentally call outer-scope builder functions, and how does @DslMarker fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

In nested blocks, both the inner and outer this receivers are in scope, so you can call an outer builder by accident. @DslMarker on the receiver types blocks the implicit outer receiver, forcing you to be explicit.

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

What are the practical trade-offs of a kotlinx.html-style markup DSL versus templating, and what design choices (inline lambdas, @DslMarker discipline, escaping, builder return types) most affect its quality?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A Kotlin DSL gives type safety, refactoring, and IDE help but couples markup to code and adds a learning curve. Good DSLs use @DslMarker for scope safety, escape output, keep blocks inline, and return useful nodes.

open as a page

How do the `getValue`/`setValue`/`provideDelegate` operator conventions support `by`-delegated properties, and how is this different from the `get`/`set` indexing operators?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Property delegation (val x by delegate) uses getValue and setValue operator functions on the delegate, with an optional provideDelegate to customize creation. The get/set operators are different — they back the [] indexing syntax, not the by keyword.

open as a page