Stop Writing Builder Boilerplate: Groovy Closures as a DSL Engine
Groovy Closures with a custom delegate let you build readable internal DSLs for configuration. Learn how delegate and resolveStrategy redirect method calls, see a tiny ConfigBuilder example, and understand the debugging and performance trade-offs.
20 Aug 2026, 02:37 UTC

Configuration code in Groovy projects often ends up as nested builder calls that read like Java, not like the domain they describe. The useful takeaway is that a Groovy Closure with a custom delegate can turn those calls into an internal DSL without annotations or code generation.
The problem is implicit method resolution, not syntax sugar
A Closure in Groovy is a first-class object. It can be passed as an argument, returned from a method, and stored in a variable. What makes it useful for DSLs is the delegate property.
Every Closure has an owner, a delegate, and a resolveStrategy. When you call a method inside the Closure body, Groovy first looks in the owner, then in the delegate depending on the strategy. Setting the delegate to a builder object lets the Closure call builder methods as if they were local.
How delegate redirects calls
With resolveStrategy set to Closure.DELEGATE_FIRST, unqualified method calls inside the Closure are resolved against the delegate before the owner. That is the mechanism behind Groovy's built-in DSLs.
Implicit parameter it is also relevant. A Closure with a single argument can refer to it without declaring a parameter, which reduces boilerplate in collection processing like list.findAll { it > 0 }.
Worked example: a tiny config DSL
Create a file ConfigDsl.groovy and run it with groovy ConfigDsl.groovy from a terminal with read access to the file. No special permissions are required.
class ConfigBuilder {
Map settings = [:]
void setting(String key, String value) { settings[key] = value }
void environment(String name, Closure c) {
def env = new ConfigBuilder()
c.delegate = env
c.resolveStrategy = Closure.DELEGATE_FIRST
c()
settings[name] = env.settings
}
void printConfig() { println settings }
}
def build(Closure c) {
def builder = new ConfigBuilder()
c.delegate = builder
c.resolveStrategy = Closure.DELEGATE_FIRST
c()
builder
}
def cfg = build {
setting 'timeout', '30s'
environment 'prod' {
setting 'url', 'https://api.example.com'
setting 'retries', '3'
}
}
cfg.printConfig()
The build method installs a ConfigBuilder as the delegate. Inside the build closure, setting and environment are resolved on the builder, not on the script. The nested environment block creates a child builder and repeats the delegate setup.
A practical way to check the result is to inspect cfg.settings after build returns, or add a println inside setting to verify which object receives the call.
Trade-offs and limits
Delegate-based DSLs hide method resolution. When a name exists in both owner and delegate, the chosen strategy decides which wins, which can lead to surprising behavior. Naming collisions are the most common source of bugs.
Debugging is harder because stack traces point to the Closure body, not the builder method that was actually invoked. Overuse of implicit resolution makes code harder to follow for readers unfamiliar with Groovy scoping.
Closures also carry runtime overhead compared to plain Java methods because each Closure is an object with its own owner/delegate chain. For hot paths processing large datasets, prefer explicit loops or Java methods and reserve DSL style for configuration and setup.
Closures can be coerced to Java interfaces such as Runnable or Callable by the Groovy runtime, which is useful for interop but does not remove the scoping complexity.
Actionable closing
Use delegate-based Closures for small, readable configuration surfaces where the domain vocabulary matters more than performance. Keep the DSL surface small, set resolveStrategy explicitly to DELEGATE_FIRST, and avoid reusing common names like with or each inside the DSL. Start with one builder, verify method resolution by printing the delegate class, and expand only when the readability gain is clear.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.