---
title: "Kotlin DSL Tutorial em Português — Passo a Passo | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-dsl-tutorial/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-dsl-tutorial.MD"
description: "Aprenda a criar DSLs em Kotlin: builder pattern, lambda with receiver, @DslMarker e type-safe builders. Tutorial completo com exemplos práticos."
date: "2025-07-28"
author: "Karina Melo"
---

# Kotlin DSL Tutorial em Português — Passo a Passo | Kotlin Brasil

Aprenda a criar DSLs em Kotlin: builder pattern, lambda with receiver, @DslMarker e type-safe builders. Tutorial completo com exemplos práticos.


Uma das funcionalidades mais poderosas do Kotlin é a capacidade de criar **Domain-Specific Languages** (DSLs) — mini-linguagens especializadas que tornam o código mais expressivo e legível. Neste tutorial, vamos explorar todos os recursos que fazem isso possível: builder pattern, [lambda](/glossario/lambda/) with receiver, a annotation `@DslMarker`, type-safe builders, e construir exemplos práticos do zero. Também vamos entender como o Gradle utiliza esses mesmos conceitos internamente.

## O que é uma DSL?

Uma [DSL](/glossario/dsl/) (Domain-Specific Language) é uma linguagem projetada para um domínio específico. Diferente de uma linguagem de propósito geral como Kotlin ou Java, uma DSL oferece uma sintaxe focada que se lê quase como linguagem natural para aquele contexto. Exemplos clássicos incluem SQL para consultas a banco de dados, HTML para markup e expressões regulares para padrões de texto.

Em Kotlin, podemos criar **DSLs internas** — código Kotlin válido que se parece com uma linguagem customizada graças a recursos como [extension functions](/glossario/extension-function/), [lambdas](/glossario/lambda/) com receiver, operadores infix e conventions de nomeação. O resultado é uma API que é ao mesmo tempo type-safe e extremamente legível.

## Passo 1: Entendendo Lambda with Receiver

O alicerce de qualquer DSL em Kotlin é a **lambda with receiver**. É uma [lambda](/glossario/lambda/) que executa no contexto de um objeto receptor, permitindo acessar suas propriedades e métodos sem qualificação:

```kotlin
// Tipo da lambda: StringBuilder.() -> Unit
fun construirString(bloco: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.bloco()  // ou bloco(sb)
    return sb.toString()
}

val resultado = construirString {
    append("Olá, ")      // 'this' e o StringBuilder
    append("Kotlin ")
    append("Brasil!")
}
println(resultado) // Olá, Kotlin Brasil!
```

Dentro da lambda, `this` referência o `StringBuilder`. Como Kotlin permite omitir `this`, chamamos `append()` diretamente, criando uma sintaxe limpa e fluente. Esse é exatamente o padrão usado por `apply`, `with` e `run` da biblioteca padrão.

## Passo 2: Builder Pattern com Lambdas

Vamos construir um builder para configurar um servidor HTTP. Primeiro, definimos as [classes](/glossario/class/) de dados:

```kotlin
data class ConfigServidor(
    val host: String,
    val porta: Int,
    val ssl: ConfigSSL?,
    val rotas: List<Rota>
)

data class ConfigSSL(
    val certificado: String,
    val chavePrivada: String
)

data class Rota(
    val caminho: String,
    val metodo: String,
    val handler: String
)
```

Agora, criamos os builders com lambdas with receiver:

```kotlin
class ServidorBuilder {
    var host: String = "localhost"
    var porta: Int = 8080
    private var ssl: ConfigSSL? = null
    private val rotas = mutableListOf<Rota>()

    fun ssl(bloco: SSLBuilder.() -> Unit) {
        ssl = SSLBuilder().apply(bloco).build()
    }

    fun rota(caminho: String, metodo: String = "GET", handler: String) {
        rotas.add(Rota(caminho, metodo, handler))
    }

    fun rotas(bloco: RotasBuilder.() -> Unit) {
        RotasBuilder(rotas).apply(bloco)
    }

    fun build(): ConfigServidor = ConfigServidor(host, porta, ssl, rotas)
}

class SSLBuilder {
    var certificado: String = ""
    var chavePrivada: String = ""
    fun build(): ConfigSSL = ConfigSSL(certificado, chavePrivada)
}

class RotasBuilder(private val rotas: MutableList<Rota>) {
    fun get(caminho: String, handler: String) {
        rotas.add(Rota(caminho, "GET", handler))
    }
    fun post(caminho: String, handler: String) {
        rotas.add(Rota(caminho, "POST", handler))
    }
    fun delete(caminho: String, handler: String) {
        rotas.add(Rota(caminho, "DELETE", handler))
    }
}

fun servidor(bloco: ServidorBuilder.() -> Unit): ConfigServidor {
    return ServidorBuilder().apply(bloco).build()
}
```

O uso final fica limpo e expressivo:

```kotlin
val config = servidor {
    host = "api.kotlinbrasil.com"
    porta = 443

    ssl {
        certificado = "/certs/server.crt"
        chavePrivada = "/certs/server.key"
    }

    rotas {
        get("/api/usuarios", "UsuarioController::listar")
        post("/api/usuarios", "UsuarioController::criar")
        delete("/api/usuarios/{id}", "UsuarioController::remover")
    }
}
```

Esse código parece uma linguagem de configuração dedicada, mas é Kotlin puro, com verificação de tipos e autocompletion do IDE.

## Passo 3: @DslMarker para Escopo Controlado

Um problema com DSLs aninhadas é que lambdas internas podem acessar receivers externos, causando confusão. A annotation `@DslMarker` resolve isso restringindo o escopo:

```kotlin
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HTML {
    private val children = mutableListOf<HtmlElement>()

    fun head(bloco: Head.() -> Unit) {
        children.add(Head().apply(bloco))
    }

    fun body(bloco: Body.() -> Unit) {
        children.add(Body().apply(bloco))
    }

    override fun toString(): String = "<html>\n${children.joinToString("\n")}\n</html>"
}

@HtmlDsl
class Head : HtmlElement {
    var titulo: String = ""

    fun title(texto: String) { titulo = texto }

    override fun toString(): String = "  <head>\n    <title>$titulo</title>\n  </head>"
}

@HtmlDsl
class Body : HtmlElement {
    private val elementos = mutableListOf<String>()

    fun h1(texto: String) { elementos.add("    <h1>$texto</h1>") }
    fun p(texto: String) { elementos.add("    <p>$texto</p>") }
    fun div(bloco: Body.() -> Unit) {
        val inner = Body().apply(bloco)
        elementos.add("    <div>\n${inner.elementos.joinToString("\n") { "  $it" }}\n    </div>")
    }

    override fun toString(): String = "  <body>\n${elementos.joinToString("\n")}\n  </body>"
}

interface HtmlElement

fun html(bloco: HTML.() -> Unit): HTML = HTML().apply(bloco)
```

Com `@DslMarker`, dentro do bloco `body { }` você **não pode** acessar diretamente os métodos de `HTML`. Isso previne erros como chamar `head { }` dentro de `body { }`:

```kotlin
val pagina = html {
    head {
        title("Kotlin Brasil")
        // body { } // ERRO de compilacao! @DslMarker impede isso
    }
    body {
        h1("Bem-vindo ao Kotlin Brasil")
        p("Aprenda Kotlin em português")
        div {
            p("Conteúdo dentro de uma div")
            // head { } // ERRO! Não está no escopo de HTML
        }
    }
}
println(pagina)
```

O `@DslMarker` é essencial para DSLs robustas. Se realmente precisar acessar um receiver externo, use `this@html` explicitamente.

## Passo 4: DSL de Configuração Prática

Vamos criar uma DSL para configuração de aplicação, um caso de uso muito comum em projetos reais:

```kotlin
@DslMarker
annotation class ConfigDsl

@ConfigDsl
class AppConfig {
    var nome: String = ""
    var versao: String = "1.0.0"
    var ambiente: String = "desenvolvimento"
    private var _banco: BancoConfig? = null
    private var _cache: CacheConfig? = null
    private val _features = mutableMapOf<String, Boolean>()

    fun banco(bloco: BancoConfig.() -> Unit) {
        _banco = BancoConfig().apply(bloco)
    }

    fun cache(bloco: CacheConfig.() -> Unit) {
        _cache = CacheConfig().apply(bloco)
    }

    fun features(bloco: FeatureFlags.() -> Unit) {
        FeatureFlags(_features).apply(bloco)
    }

    fun build(): Map<String, Any?> = mapOf(
        "nome" to nome,
        "versao" to versao,
        "ambiente" to ambiente,
        "banco" to _banco,
        "cache" to _cache,
        "features" to _features
    )
}

@ConfigDsl
class BancoConfig {
    var url: String = "jdbc:postgresql://localhost:5432/app"
    var usuario: String = "postgres"
    var senha: String = ""
    var poolSize: Int = 10
    var timeout: Long = 30_000
}

@ConfigDsl
class CacheConfig {
    var provedor: String = "redis"
    var host: String = "localhost"
    var porta: Int = 6379
    var ttlSegundos: Long = 3600
}

@ConfigDsl
class FeatureFlags(private val flags: MutableMap<String, Boolean>) {
    infix fun String.habilitada(valor: Boolean) {
        flags[this] = valor
    }
}

fun appConfig(bloco: AppConfig.() -> Unit): Map<String, Any?> {
    return AppConfig().apply(bloco).build()
}
```

O uso dessa DSL demonstra como a configuração se torna autodocumentada:

```kotlin
val config = appConfig {
    nome = "Kotlin Brasil API"
    versao = "2.1.0"
    ambiente = "producao"

    banco {
        url = "jdbc:postgresql://db.exemplo.com:5432/producao"
        usuario = "app_user"
        senha = System.getenv("DB_SENHA") ?: ""
        poolSize = 20
        timeout = 15_000
    }

    cache {
        provedor = "redis"
        host = "cache.exemplo.com"
        ttlSegundos = 1800
    }

    features {
        "novo-dashboard" habilitada true
        "beta-relatorios" habilitada false
        "notificacoes-push" habilitada true
    }
}
```

Note o uso da função `infix` para `habilitada`, criando uma sintaxe natural para feature flags. Funções infix eliminam a necessidade de ponto e parênteses, aproximando o código de prosa.

## Passo 5: Como o Gradle DSL Funciona Internamente

O `build.gradle.kts` que usamos diariamente é construído exatamente com essas técnicas. Vamos desmistificar:

```kotlin
// Simplificação do que o Gradle faz internamente
class Project {
    fun dependencies(bloco: DependencyHandler.() -> Unit) {
        DependencyHandler().apply(bloco)
    }

    fun repositories(bloco: RepositoryHandler.() -> Unit) {
        RepositoryHandler().apply(bloco)
    }
}

class DependencyHandler {
    fun implementation(dependencia: String) {
        println("Adicionando: $dependencia ao classpath de compilacao")
    }
    fun testImplementation(dependencia: String) {
        println("Adicionando: $dependencia ao classpath de teste")
    }
}

class RepositoryHandler {
    fun mavenCentral() { println("Repositório: Maven Central") }
    fun google() { println("Repositório: Google") }
}
```

Quando você escreve `dependencies { implementation("...") }` no Gradle, está usando exatamente o mesmo padrão de lambda with receiver que construímos neste tutorial. O bloco `plugins { }`, `repositories { }`, `tasks { }` — todos seguem esse padrão. Agora você entende a magia por trás do `build.gradle.kts`.

## Erros Comuns

**1. Não usar `@DslMarker`:** Sem ele, DSLs aninhadas permitem acessar receivers de escopos externos, levando a configurações incorretas que compilam sem erros. Sempre crie uma annotation marcada com `@DslMarker` para sua DSL.

**2. Builders mutáveis expostos:** Nunca exponha o builder diretamente como resultado. Sempre crie um método `build()` que retorna um [objeto](/glossario/object/) imutável ([data class](/glossario/data-class/) ou similar). Isso garante que a configuração não pode ser alterada após a construção.

**3. Excesso de DSL:** Nem tudo precisa ser uma DSL. Use DSLs quando a mesma estrutura será configurada repetidamente e a legibilidade é crítica. Para configurações simples, um construtor com parâmetros nomeados pode ser suficiente.

**4. Esquecer de chamar `apply`:** Um erro sutil é escrever `Builder().bloco()` ao invés de `Builder().apply(bloco)`. Com `apply`, o `this` dentro da lambda é o Builder. Sem ele, dependendo do tipo da lambda, o comportamento pode ser diferente.

**5. Lambda with receiver vs lambda comum:** `(String) -> Unit` e `String.() -> Unit` são tipos diferentes. O primeiro recebe String como parâmetro, o segundo usa String como receiver. Confundir os dois causa erros de tipo difíceis de diagnosticar para iniciantes.

## Conclusão e Próximos Passos

Neste tutorial, exploramos os fundamentos de DSLs em Kotlin: lambdas with receiver para contextos fluentes, builder pattern para construção declarativa, `@DslMarker` para escopo seguro, funções infix para sintaxe natural e type-safe builders para configurações robustas. Esses conceitos são a espinha dorsal de bibliotecas como Ktor, Gradle, Compose e Exposed.

Como próximos passos, experimente criar uma DSL para o domínio do seu projeto — seja para configuração de testes, definição de rotas de API ou regras de válidação. Estude o código-fonte do Ktor e do Compose para ver como DSLs são usadas em escala de produção. Confira também nosso tutorial sobre [Gradle com Kotlin DSL](/tutoriais/kotlin-gradle-tutorial/) para ver essas técnicas aplicadas no sistema de build, e o [glossário de DSL](/glossario/dsl/) para uma referência rápida do conceito.
