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.
| Strategy | Search order |
|---|---|
OWNER_FIRST (default) | Owner, then delegate |
DELEGATE_FIRST | Delegate, then owner |
OWNER_ONLY | Owner only |
DELEGATE_ONLY | Delegate only |
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.
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.
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.
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
- Use
DELEGATE_FIRSTfor DSL blocks. The block should see the configuration's methods first, so names read the way the DSL author intended. - Return the configured object. A DSL function that returns its result makes it easy to assign the configuration to a variable.
- Add
@DelegatesToto public DSL methods. It enables static checking and IDE completion for the block's contents. - Keep the vocabulary small. A few clear verbs such as
host,port, androuteare easier to read than many special-purpose methods. - Restore state when the closure is done. If a builder keeps
a stack or a depth counter, change it in a
try/finallyblock so an error inside the block does not leave the builder inconsistent.
Source
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
List all Groovy tutorials.