ZetCode

Groovy Builders and DSLs

last modified October 15, 2026

A domain-specific language (DSL) is a small notation built for one task. Groovy is well suited to DSLs because of its flexible syntax: method calls without parentheses, trailing closures, and the ability to change what a closure's unqualified names refer to. Together these let a configuration read almost like a sentence. This tutorial shows how closures with a delegate are used to build such notations, how to control name resolution, how to give closures type hints for @CompileStatic, and how to write a nested builder for markup.

The basics of closures, including delegate and DELEGATE_FIRST, are covered in the Groovy closures tutorial. The @Builder annotation that generates a fluent builder is covered in the Groovy AST transformations tutorial. This article focuses on writing DSLs yourself.

How a Closure Resolves Names

When a closure calls a method or reads a property, Groovy searches for it in two places: the owner, which is the class or closure where the closure was written, and the delegate, an object that can be set at runtime. The resolveStrategy property decides the search order.

StrategySearch order
OWNER_FIRST (default)Owner, then delegate
DELEGATE_FIRSTDelegate, then owner
OWNER_ONLYOwner only
DELEGATE_ONLYDelegate only
Strategies.groovy
class Inner {
    String who() { 'delegate' }
}

class Owner {
    String who() { 'owner' }

    Closure makeClosure() {
        return { who() }
    }
}

def owner = new Owner()
def strategies = [
    'OWNER_FIRST', 'DELEGATE_FIRST', 'OWNER_ONLY', 'DELEGATE_ONLY'
]
strategies.each { name ->
    def c = owner.makeClosure()
    c.delegate = new Inner()
    c.resolveStrategy = Closure."$name"
    println "$name -> ${c()}"
}

Every closure here calls who(). Both the owner and the delegate define it, so the strategy decides which answer is used. The closure is created inside Owner, so its owner is an Owner object.

Builders and DSLs almost always use DELEGATE_FIRST. The code inside the block should see the configuration object's methods before anything else.

$ groovy Strategies.groovy
OWNER_FIRST -> owner
DELEGATE_FIRST -> delegate
OWNER_ONLY -> owner
DELEGATE_ONLY -> delegate

A Configuration DSL

The following example defines a server function that takes a closure. The closure runs with a ServerConfig object as its delegate, so the calls inside the block set the object's fields.

ServerDsl.groovy
class ServerConfig {
    String host = 'localhost'
    int port = 80
    List<String> routes = []

    void host(String h) { host = h }
    void port(int p) { port = p }
    void route(String r) { routes << r }
    String toString() { "Server($host:$port, routes=$routes)" }
}

def server(Closure cfg) {
    def config = new ServerConfig()
    cfg.resolveStrategy = Closure.DELEGATE_FIRST
    cfg.delegate = config
    cfg()
    config
}

def s = server {
    host 'example.com'
    port 8443
    route '/api'
    route '/health'
}
println s

Inside the block, host 'example.com' is a method call with no parentheses. Because the delegate is config, Groovy finds host(String) on it. The function then returns the configured object, so the caller gets a value it can use.

cfg.resolveStrategy = Closure.DELEGATE_FIRST
cfg.delegate = config
cfg()

This is the core pattern of a closure DSL: set the delegate, set the strategy, and call the closure. The three lines run the user's block against the configuration object.

$ groovy ServerDsl.groovy
Server(example.com:8443, routes=[/api, /health])

Typing a DSL with @DelegatesTo

Static compilation does not know what a closure's delegate is, so calls such as host 'example.org' inside a block are reported as errors. The @DelegatesTo annotation on the closure parameter tells the compiler which type the delegate has.

StaticDsl.groovy
import groovy.transform.CompileStatic

class ServerConfig {
    String host = 'localhost'
    void host(String h) { host = h }
}

ServerConfig server(@DelegatesTo(value = ServerConfig, strategy = Closure.DELEGATE_FIRST) Closure cfg) {
    def config = new ServerConfig()
    cfg.resolveStrategy = Closure.DELEGATE_FIRST
    cfg.delegate = config
    cfg()
    config
}

@CompileStatic
void demo() {
    def s = server {
        host 'example.org'
    }
    println s.host
}
demo()

The value attribute names the delegate type, and strategy records the resolution order the closure expects. With these hints, the call inside demo is checked against ServerConfig, so a misspelled method name is reported at compile time.

Without the @DelegatesTo annotation on server, the same @CompileStatic method fails with Cannot find matching method ... #host(java.lang.String). Add the hint whenever a DSL is used from statically compiled code, and it also helps IDEs offer completion inside the block.

$ groovy StaticDsl.groovy
example.org

A Nested Markup Builder

Closures can also nest, which makes them a good fit for tree-shaped output such as HTML. The builder below handles any tag name through methodMissing. Each call takes an optional map of attributes, a string of text, or a closure for child elements.

HtmlDsl.groovy
class HtmlBuilder {
    private StringBuilder out = new StringBuilder()
    private int depth = 0

    def methodMissing(String name, args) {
        def attrs = args && args[0] instanceof Map ? args[0] : [:]
        def body = args.find { it instanceof Closure }
        def text = args.find { it instanceof String }
        out << '  ' * depth << "<$name"
        attrs.each { k, v -> out << " $k=\"$v\"" }
        out << '>'
        if (body) {
            out << '\n'
            depth++
            body.delegate = this
            body.resolveStrategy = Closure.DELEGATE_FIRST
            body()
            depth--
            out << '  ' * depth
        } else {
            out << (text ?: '')
        }
        out << "</$name>\n"
        this
    }

    String toString() { out.toString() }
}

def html = new HtmlBuilder()
html.html {
    body(class: 'main') {
        h1 'Hello DSL'
        p 'A paragraph'
    }
}
print html

Calling html.html { ... } or body(...) does not match any method that HtmlBuilder declares, so Groovy calls methodMissing with the tag name as name. The builder writes the opening tag, runs the child closure, and then writes the closing tag. Because nested calls return this, the same object keeps collecting output.

body.delegate = this
body.resolveStrategy = Closure.DELEGATE_FIRST
body()

The child closure's delegate is the builder itself. When the closure calls h1 or p, those names go to methodMissing, which is why the child elements appear inside the parent tag.

For real HTML or XML output, Groovy's built-in MarkupBuilder is the better choice. It is covered in the Groovy XML tutorial. The custom builder above shows the mechanism that makes such builders work.

$ groovy HtmlDsl.groovy
<html>
  <body class="main">
    <h1>Hello DSL</h1>
    <p>A paragraph</p>
  </body>
</html>

Best Practices

Source

Groovy documentation

This tutorial showed how closure delegation and resolution strategies work, how to write a configuration DSL, how @DelegatesTo types the closure for static compilation, and how a nested builder uses methodMissing to turn method calls into markup.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than eight years of experience in teaching programming.

List all Groovy tutorials.