Knowledge Map
Kotlin DSL
Every concept from basic lambdas to advanced type-safe builder patterns — with a plain-English sentence explaining what each one means.
145
Concepts
19
Groups
84
Must-know
Fundamental
What is a DSL, lambdas, receivers, builders & the core language toolsDomain Specific Language
A mini-language designed for one specific problem — Kotlin lets you build DSLs that look like their own syntax inside regular Kotlin code.
Internal DSL
A DSL written entirely in the host language (Kotlin) — it reuses Kotlin's parser and type checker rather than defining a new grammar.
External DSL
A separate language with its own parser and syntax — SQL, HTML, and Gradle's Groovy scripts are classic examples.
Fluent API
An API designed to be read like natural language — method chains where each call returns a meaningful object to continue the chain.
Builder Pattern
A design pattern that constructs complex objects step by step — Kotlin replaces the verbose Java version with concise lambdas.
Readability Goal
The primary purpose of a DSL is to make code read like a description of the problem rather than instructions to a machine.
Real-world DSLs in Kotlin
Jetpack Compose UI, Gradle build scripts, Ktor routing, kotlinx.html, and Exposed SQL are all Kotlin DSLs you may already use.
Lambda Expression
An anonymous function written as { params -> body } — the raw building block every DSL feature is assembled from.
Higher-Order Function
A function that takes another function as a parameter — this is how DSL blocks like apply {} and buildString {} work.
Trailing Lambda Syntax
When a lambda is the last argument, you move it outside the parentheses — this is why DSL blocks look like language syntax rather than function calls.
Function Type
A type that describes a function's shape — () -> Unit means "a function that takes nothing and returns nothing."
Lambda Capturing
A lambda remembers and can read or modify variables from the enclosing scope where it was defined.
it (implicit parameter)
When a lambda has exactly one parameter, you can skip naming it and use it as the automatic shorthand.
Anonymous Function
A function literal written with the fun keyword but without a name — an alternative to lambda syntax with explicit return types.
Callable Reference (::)
Turns an existing named function into a lambda value using :: — pass it anywhere a function type is expected.
Lambda with Receiver
A lambda that has a specific this — inside it you call the receiver's members directly without any qualifier, just like being inside a class.
Receiver Type Syntax
Written as ReceiverType.() -> Unit — the dot before the parentheses is what makes it a lambda with receiver instead of a plain lambda.
this inside a receiver lambda
Refers to the object the lambda was called on — you can call all its public functions and access its properties without prefixes.
Implicit this
Because this is implicit, you can write just name = "John" inside a builder block instead of builder.name = "John".
apply {} — simplest example
The built-in scope function that passes an object as this to a lambda — the simplest demonstration of lambda with receiver.
Receiver vs Extension Function
An extension function is a named lambda with receiver — both share the same underlying mechanism but syntax differs.
buildString {}
A stdlib function that gives you a StringBuilder as this inside the block — classic lambda-with-receiver usage.
with(obj) {}
A non-extension scope function that sets obj as this inside the block — another direct demonstration of receiver lambdas.
Extension Function
Adds a new function to an existing class — DSLs use these heavily so users call dsl.configure() as if it's a built-in member.
Extension on DSL Builder
Adding functions directly to your builder class via extensions keeps the DSL's core clean while allowing external contributors to add verbs.
Chaining Extensions
Because every extension returns the same type, you can dot-chain calls like a sentence — obj.setName("A").setAge(30).build().
Infix Extension
An extension function marked infix can be called without dots or parentheses — reads like natural language: user shouldHave role.
Operator Extension
An extension that overloads a Kotlin operator like + or [] — allows intuitive syntax like list += item inside your DSL.
Extension Properties
Adds a computed property to an existing class — lets DSL users read config.isValid instead of calling config.checkValidity().
Nullable Receiver Extension
An extension on a nullable type — lets DSL helper functions handle null gracefully without forcing null checks on the caller.
apply {}
Gives you the object as this inside the block and returns the object — the standard idiom for configuring a freshly created builder.
also {}
Gives you the object as it and returns the object — use it for side effects like logging or registering without breaking a chain.
let {}
Gives you the object as it and returns the lambda's result — ideal for null-safe transformations inside DSL chains.
run {}
Gives you the object as this and returns the lambda's result — use it when a builder block should compute and return a value.
with(obj) {}
Not an extension — takes the object as its first argument and sets it as this in the block, returning the result.
takeIf {} / takeUnless {}
Returns the object or null based on a condition — useful inside DSL chains where you want to conditionally continue.
Choosing the Right Scope Function
Use apply/with for configuration (this + return object), let/run for transformation (result), also for side effects (it + return object).
Nested Scope Functions
Scope functions can be nested but require care — the inner this shadows the outer one, which can cause subtle bugs.
operator fun
The keyword prefix that overloads a standard Kotlin operator for your custom type — only predefined operators can be overloaded.
plus / minus / times
Overloading + - * operators lets users write intuitive arithmetic-style DSL expressions like config + feature.
invoke()
Overloading the () call operator lets an object instance be called like a function — factory DSLs use this heavily.
get() / set()
Overloading [] index access lets DSL users write config["key"] = value instead of config.put("key", value).
contains()
Overloading the in operator lets DSL users write item in collection as a readable membership check.
rangeTo() (..)
Overloading .. lets your DSL express ranges — Monday..Friday reads cleanly in scheduling or date-range DSLs.
unaryMinus / unaryPlus
Unary operator overloads — -item can mean "exclude this item" in a set-building DSL, making exclusions readable.
compareTo()
Overloads < > <= >= for your type — enables natural comparison expressions inside filter or ordering DSLs.
Intermediate
Type-safe builders, nesting, generics, infix & real-world DSL patternsType-Safe Builder
A DSL pattern where the compiler enforces that only legal configurations can be expressed — invalid nesting is a compile error, not a runtime crash.
Builder Class
A class whose sole purpose is to accumulate configuration through DSL calls and then produce a finished object via build().
Entry Function
The top-level function (like html {}, buildRoute {}) that creates the builder, passes it to a receiver lambda, and returns the result.
Nested Builder Functions
Functions on the builder that accept their own receiver lambdas — this is how html { body { p { } } } achieves clean nesting.
build() pattern
The terminal function that reads all accumulated state from the builder and constructs the final immutable object.
Collecting children
The builder pattern of holding a mutable list and appending child objects as nested DSL blocks call add-like functions.
Immutable output
The final object produced by a builder is typically immutable — the builder is mutable during setup, the result never changes.
DSL vs Constructor
Constructors are good for simple objects; DSLs shine when an object has many optional parts, nested children, or readable configuration blocks.
kotlinx.html as example
Kotlin's HTML builder library is the canonical type-safe builder — div { p { +"text" } } generates valid HTML checked at compile time.
@DslMarker
A meta-annotation you define and apply to builder classes — it prevents accidentally calling an outer builder's functions from inside an inner block.
Implicit Receiver Leak
The bug @DslMarker prevents — without it, an inner block can silently call functions on an outer receiver, creating confusing misbehaviours.
Defining a DslMarker
Declare @DslMarker annotation class MyDsl and annotate each builder class with @MyDsl — one annotation controls the whole hierarchy.
Scope Enforcement
Once @DslMarker is applied, calling an outer-scope function from an inner block becomes a compile-time error — the scope is explicit and safe.
Multiple DSL Scopes
Different DSL families each get their own @DslMarker annotation so their builders don't interfere with each other when nested.
@HtmlTagMarker example
kotlinx.html uses @HtmlTagMarker to prevent writing a div inside a span's block — each HTML element enforces its own scope.
Explicit this@OuterScope
When you genuinely need the outer receiver inside an inner block, qualify it with this@BuilderName to make the intent explicit.
Generic Builder
A builder parameterised on the type it builds — class Builder<T> lets one builder class serve many output types.
Type-Safe add functions
Generic add functions on a builder ensure only the correct type of child can be added — wrong types are rejected at compile time.
reified + inline
Using reified type parameters lets DSL functions branch on the actual type at call sites — no Class<T> parameter needed.
Bounded Type Parameters
Constraining <T : BaseConfig> ensures only valid config types can be passed to a generic DSL function — catches misuse at compile time.
Covariant DSL Output
Marking a builder's result type as out lets you assign Builder<Dog> where Builder<Animal> is expected in collection DSLs.
Generic Receiver Lambda
A function accepting T.() -> Unit works for any T — the receiver type is inferred from context at the call site automatically.
Star Projection in DSLs
Using Builder<*> when you need to accept any builder regardless of its type — the wildcard of Kotlin's generic system.
typealias for DSL types
Giving a long generic function type a short alias — typealias BuilderBlock = Builder.() -> Unit makes signatures readable.
infix fun
A single-parameter function that can be called without dots or parentheses — a to b reads better than a.to(b) in many DSLs.
Natural Language Readability
Infix calls bridge the gap between code and prose — user should have permission reads like a specification, not a function call.
to (Pair creation)
The most widely used infix function in Kotlin — "key" to "value" creates a Pair and is the backbone of mapOf() DSL syntax.
Infix as DSL Connector
Chaining infix calls creates readable assertion or routing DSLs — GET path "/home" respondsWith handler looks like a rule definition.
Precedence Rules
Infix calls have lower precedence than arithmetic operators but higher than assignments — be explicit with parentheses in complex expressions.
Testing DSLs with infix
Libraries like Kotest use infix heavily — result shouldBe expected reads like natural English and makes tests self-documenting.
Combining infix + operator
Mixing infix functions with operator overloads lets you build expressive mini-languages — Monday..Friday every 2.hours reads naturally.
Jetpack Compose
The entire Compose UI is a DSL — @Composable functions with trailing lambdas create a declarative UI tree that reads like a layout description.
Ktor Routing DSL
routing { get("/home") { call.respond("Hello") } } — a nested receiver-lambda DSL where each block tightens the routing scope.
Gradle Kotlin DSL
The build.gradle.kts file is itself a DSL — dependencies { implementation(libs.retrofit) } uses receiver lambdas throughout.
kotlinx.html
HTML generation as a Kotlin type-safe builder — every HTML tag is a function, nesting is enforced at compile time.
Exposed SQL DSL
Database queries written as Kotlin expressions — Users.select { Users.age greater 18 } is compiled SQL, not a string.
Anko (legacy)
JetBrains' now-deprecated DSL for building Android layouts in Kotlin — a historical example of applying DSL patterns to view construction.
Testing DSLs (Kotest / MockK)
every { mock.method() } returns value and result shouldBe expected are DSLs that make test setup and assertions read like specifications.
Coroutine DSL (flow {})
The flow {} builder is a receiver DSL where emit() is the only available action — it enforces that you can only produce, not consume.
What are precompiled script plugins
Gradle .gradle.kts scripts in src/main/kotlin that are compiled into reusable plugins applied by ID.
buildSrc convention
Putting convention scripts in buildSrc/src/main/kotlin makes them available automatically to all subprojects.
build-logic as a separate build
The modern approach: use includeBuild("build-logic") to avoid broad buildSrc invalidations.
Applying a convention plugin
Apply with plugins { id("my.android.library") } in a module to pull in shared Gradle defaults.
Type-safe accessors
Kotlin DSL generates strongly typed accessors for catalogs and plugin aliases instead of raw string keys.
Sharing dependencies across modules
Centralize dependency and plugin conventions once so subprojects inherit consistent versions and configuration.
Mutable Properties on Builder
Declaring var properties on a builder lets DSL users set values with simple assignment — name = "Alice" feels like filling a form.
Property vs Function in DSL
Use properties for simple values (title = "Hello") and functions for sub-blocks or actions (body { }) — keeps the DSL grammar intuitive.
Default Values
Setting sensible defaults on builder properties means users only write what they want to change — reduces noise and cognitive load.
Delegated Properties in Builders
Using by observable {} on builder properties lets you react to assignments — validate, transform, or log every change in real time.
Read-Only DSL Properties
Computed val properties on a builder expose derived state — inside a builder config.summary reads a computed value not directly settable.
lateinit in Builders
Marking a required builder property lateinit and checking .isInitialized in build() lets you give a clear error for missing required fields.
backing MutableList
A common DSL pattern — expose a read-only List externally but collect children into a private MutableList through add-like DSL calls.
Advanced
Context receivers, inline, contracts, compiler plugins & DSL design theoryinline fun
The compiler copies the function body to every call site — eliminates the object allocation for lambdas and enables reified generics.
Why inline matters for DSLs
Every DSL block { } creates a lambda object at runtime unless the enclosing function is inline — inline makes large DSLs allocation-free.
reified type parameter
Only available in inline functions — lets the function access the actual runtime type of T, enabling type-based branching inside a DSL.
noinline
Opts one lambda parameter out of inlining — use it when you need to store the lambda or pass it to a non-inline function.
crossinline
Prevents non-local returns inside a lambda that will be called from a different execution context — required for lambdas passed to callbacks.
Non-local return
A return inside an inline lambda that exits the enclosing function — only possible because the lambda is inlined into the caller's body.
Inlining cost
Heavy inline functions with large bodies bloat bytecode — prefer inlining small DSL entry points, not complex multi-step logic.
inline class (value class)
A wrapper class erased at runtime to its single property — use it in DSLs to give primitive values semantic names with zero overhead.
context(A, B) fun
Declares that a function requires multiple implicit receivers — both A and B are available as this inside the function body.
Why Context Receivers for DSLs
They let a DSL function depend on capabilities (Logger, Transaction) without threading those dependencies through every call in the chain.
Multiple Implicit Receivers
Unlike a single lambda with receiver, context receivers stack multiple this values — each can be called by name with this@TypeName.
Capability-Based DSL Design
Modelling DSL scopes as capabilities — a function only compiles when the right context (Database, Auth) is in scope at the call site.
Context vs Extension Receiver
Extension receiver is the primary this; context receivers are secondary — a function can have both a primary receiver and context requirements.
Composing Contexts
Nesting lambdas with different context requirements builds up a set of ambient capabilities — like capability-based programming inside a DSL.
Status: Experimental
Context receivers are stable in Kotlin 2.x as context parameters — the syntax is evolving, so check the current Kotlin docs before adopting.
contract {}
A block inside a function that gives the compiler extra information about the function's behaviour — improves smart casts and flow analysis.
callsInPlace
Tells the compiler that a lambda parameter is invoked exactly once — allows variables initialised inside the lambda to be treated as definitely initialised.
EXACTLY_ONCE
The InvocationKind that promises the lambda runs once and only once — unlocks val initialisation inside DSL builder blocks.
AT_LEAST_ONCE
Tells the compiler the lambda will run one or more times — variables assigned in the block can be read after the call without error.
returns() implies
Tells the compiler "if this function returns normally, then this condition is true" — enables smart casting after a custom null-check function.
returnsNotNull() implies
Tells the compiler the function's return value is non-null — removes the need for !! after a custom check-and-return function.
@OptIn(ExperimentalContracts)
Contracts are still experimental API — you must opt in to use them and accept that the syntax may change in future releases.
Kotlin Compiler Plugin
An extension that hooks into the Kotlin compiler — lets you add new syntax, validation, or code generation that no normal library can do.
Compose Compiler Plugin
Transforms @Composable functions into state-machine code — proves that deep DSL-like behaviour can require compiler-level support.
IR (Intermediate Representation)
The compiler's internal tree that plugins manipulate — modifying the IR lets you rewrite, add, or remove code before it becomes bytecode.
KSP (Kotlin Symbol Processing)
A lighter alternative to full compiler plugins — reads your code's symbols to generate new source files, used to generate DSL boilerplate.
KAPT (Annotation Processing)
The older annotation processing tool — reads annotations to generate code, though KSP is faster and more Kotlin-aware.
kotlinx.serialization plugin
A compiler plugin that generates serialization code for @Serializable classes — no reflection needed at runtime.
Parcelize plugin
Generates Android Parcelable boilerplate for @Parcelize classes at compile time — a focused compiler plugin as DSL.
Principle of Least Surprise
DSL syntax should do exactly what it reads like — users should never need to read source code to understand what a block does.
Make Invalid States Unrepresentable
The best DSL design makes it a compile error to configure something incorrectly — push validation left from runtime to compile time.
Discoverable API
DSL users should be guided by IDE autocomplete — only expose the functions that are valid in the current context, hide the rest.
Separation of Configuration and Execution
Build the description in DSL blocks; trigger execution only after all configuration is collected — the two phases should never mix.
Avoid Deep Nesting
More than 3–4 levels of nesting makes DSL code hard to read — flatten the hierarchy where possible or split into named functions.
Prefer Functions over Properties for Sub-blocks
Use functions to introduce nested scopes (body { }) and properties for leaf values (title = "x") — the convention keeps grammar intuitive.
Stable DSL vs Evolving DSL
DSLs are hard to version — breaking changes force users to rewrite readable code, so invest in design upfront before publishing.
Testing a DSL
Write tests using the DSL itself — if the test code reads like a specification, the DSL is achieving its goal of expressiveness.
DSL documentation
Document the mental model, not just the API — show a complete example first, then explain individual functions as vocabulary of that example.
Phantom Types
Using unused type parameters to track builder state at the type level — Builder<Incomplete> vs Builder<Complete> prevents calling build() too early.
Step Builder Pattern
A DSL where each configuration step returns a different interface — enforces calling required steps in order before build() becomes available.
Delegation in DSLs
A builder delegates its storage to a separate class — enables composable builders where mixins add sections to a shared configuration object.
Recursive DSL
A builder function that returns the same builder type — enables arbitrarily deep trees like menus, file systems, or syntax trees.
Lazy DSL Evaluation
Storing receiver lambdas instead of executing them immediately — the DSL describes a program that runs later, not eagerly during configuration.
Open vs Closed DSL
An open DSL lets users add their own verbs via extension functions; a closed DSL seals the vocabulary to what the author provides.
DSL Interpreter Pattern
The builder produces a data structure (AST), and a separate interpreter walks it — separates intent from execution for testability.
Free Monad DSL
A purely functional pattern where DSL operations are data objects suspended in a structure that an interpreter later executes — testable by design.
Suspend DSL
A DSL entry function marked suspend — the block runs inside a coroutine scope, enabling async operations as first-class DSL verbs.
No concepts found matching your search.