Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
World desk5 min

How to Develop a DSL in Kotlin: A Practical Type-Safe Builder

Kotlin DSLs use ordinary typed functions with receiver lambdas. Learn to model the domain, build nested scopes safely, and decide when builder inference is useful.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Develop a Kotlin DSL by modeling the domain first, then exposing clearly named operations through functions with receiver lambdas. The result can look declarative at the call site while remaining ordinary Kotlin code with types and compile-time checks. For nested builders, plan receiver scope deliberately; for generic builders, make sure their API gives Kotlin enough information to infer type parameters.

What a Kotlin DSL is—and what it is not

A Kotlin DSL is a library API designed to make code for a particular domain read naturally. A type-safe builder commonly uses functions whose parameters are lambdas with receivers. Inside the lambda, the receiver’s members become available as if they were local operations. The block is still statically typed Kotlin, not a separate language or a string interpreted at runtime.

Kotlin’s type-safe builders guide describes this pattern: “By using well-named functions as builders in combination with function literals with receiver, it is possible to create type-safe, statically-typed builders in Kotlin.”

Start with the domain model

Decide what the DSL represents before designing its surface syntax. Identify the objects or nodes in the domain, how they relate, and which combinations are valid. A builder is useful when it makes a complex hierarchy easier to construct without hiding its structure.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Kotlin’s official HTML builder illustrates the approach: elements such as html, head, and body correspond to a hierarchical model, and named functions create and nest those elements. The same idea can apply to configuration, query descriptions, or other naturally nested data, provided the model itself is clear.

  • List the domain concepts the caller needs to create or configure.
  • Define the valid relationships between them, so the API does not invite invalid structures.
  • Choose which values should be arguments, properties, or nested blocks.

Build the API with receiver lambdas

A common builder function accepts a lambda with receiver, creates or configures a receiver object, applies the block, and returns a useful result. For example, the shape might be fun section(block: Section.() -> Unit): Section. Within the block, operations declared on Section are directly available.

The following simplified example demonstrates the mechanism; it is illustrative rather than a complete production DSL:

class Page {
    val sections = mutableListOf<Section>()

    fun section(block: Section.() -> Unit) {
        sections += Section().apply(block)
    }
}

class Section {
    val paragraphs = mutableListOf<String>()

    fun paragraph(text: String) {
        paragraphs += text
    }
}

fun page(block: Page.() -> Unit): Page = Page().apply(block)

val document = page {
    section {
        paragraph("A typed builder keeps this operation in Kotlin.")
    }
}

Here, page creates the outer receiver, while section creates and configures a nested one. The call site reads like a description, but the operations have ordinary Kotlin declarations and types. In a real implementation, choose return types and mutability to fit the domain—for example, a builder may return an immutable model rather than expose mutable collections.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make invalid structures difficult to express

Type safety is not automatic merely because a builder uses lambdas. It depends on the model and available operations. If a child is only valid under one kind of parent, represent that relationship in the receiver types or in the operations offered by each receiver. If every operation is available everywhere, the syntax may look DSL-like while still allowing nonsensical combinations.

Prefer a small, domain-focused set of receivers and well-named functions. Compare the block with a conventional API using constructors, named arguments, properties, or configuration functions. Kotlin’s API readability guidance notes: “A Kotlin library can significantly improve readability by providing a builder DSL.” That is a possible benefit, not a reason to add a DSL where a simpler API already communicates the domain well.

Control nested receiver scope with a DSL marker

Nested receiver lambdas can make members of more than one receiver implicitly visible. That may let a call intended for an inner object resolve to an unrelated outer object, making mistakes harder to spot. If the DSL has multiple receiver types, decide explicitly whether outer operations should remain implicitly callable.

A shared annotation marked with @DslMarker lets the compiler restrict implicit access to the nearest receiver carrying that marker. Apply the marker consistently to the relevant receiver classes or receiver function types. When access to an outer receiver is intentional, qualify it explicitly, for example with this@page.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
@DslMarker
annotation class PageDsl

@PageDsl
class Page {
    fun section(block: Section.() -> Unit) {
        Section().apply(block)
    }
}

@PageDsl
class Section {
    fun paragraph(text: String) { /* add paragraph */ }
}

With both receivers marked, the marker communicates that nested calls should target the closest DSL scope by default. Explicit qualification remains an escape hatch when an outer operation is genuinely needed. See the Kotlin type-safe builders guide for the language’s builder and receiver-scope behavior.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Use builder inference only when it helps

Generic builders can sometimes infer type parameters from operations inside the builder lambda, avoiding explicit type arguments at the call site. First check whether arguments to the builder function or the expected result type already provide enough information. If not, builder inference may help when the lambda receiver incorporates the relevant type parameters and exposes members or extensions whose signatures reveal them.

Kotlin’s builder inference documentation explains the requirements and behavior. In particular, do not use a type parameter directly as the builder lambda’s receiver type; that form is unsupported for builder inference. Design the receiver as a type that uses the type parameter and offers meaningful operations instead.

The same documentation says builder inference is enabled by default starting with Kotlin 1.7.0. Before 1.7.0, the documented opt-in was the compiler option -Xenable-builder-inference. Check the Kotlin compiler version configured for the project before relying on version-specific behavior.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Evaluate whether a DSL fits the problem

A builder DSL is a readability choice, not a requirement. Use these questions to assess the trade-off:

  • Type safety: Do receiver types and operations rule out invalid structures at compile time?
  • Readability: Is the block clearer than constructors, named arguments, or ordinary configuration calls?
  • Scope clarity: Can readers tell which receiver owns an operation, especially inside nested lambdas?
  • Inference and API complexity: Does inference remove noise, or make the API harder to understand and diagnose?
  • Domain fit: Is the subject naturally hierarchical or declarative, or would a plain function API be more direct?

If the block improves clarity while preserving understandable types and scope, a DSL can be a good fit. If it requires many special rules to make simple operations readable, prefer the conventional API.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.