Groovy DSL and Builder patterns with 10+ examples. Master MarkupBuilder, CliBuilder, ObjectGraphBuilder, delegate strategy, and custom DSL creation.
“A well-designed DSL reads like a conversation between the developer and the domain. Groovy makes writing those conversations remarkably easy.”
Martin Fowler, Domain-Specific Languages
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 25 minutes
If you have used Gradle, you have already used a Groovy DSL without realizing it. That clean, readable syntax in build.gradle — where you write dependencies { implementation 'some:lib:1.0' } — is not some special language. It is pure Groovy code, structured with closures and delegate strategies to look like a domain-specific language.
This is one of Groovy’s most useful capabilities. The language was designed from the ground up to support DSL creation. Features like optional parentheses, closure delegates, operator overloading, and built-in builders make it possible to write code that reads like English while still being fully executable Groovy.
In this tutorial, we will explore Groovy DSLs and the Builder pattern with 10+ tested examples. You will learn how MarkupBuilder generates HTML and XML, how CliBuilder parses command-line arguments, how delegate strategies work, and how to create your own custom DSLs. Here are the examples.
Table of Contents
What Is a DSL?
A Domain-Specific Language (DSL) is a small, focused language designed for a specific problem domain. Unlike general-purpose languages like Java or Python, a DSL provides a vocabulary and syntax tailored to one task. According to the official Groovy DSL documentation, there are two types:
| Type | Description | Examples |
|---|---|---|
| External DSL | Separate language with its own parser | SQL, HTML, CSS, Regular Expressions |
| Internal DSL | Embedded within a host language | Gradle build files, Spock tests, Grails configurations |
Groovy excels at internal DSLs — DSLs that are valid Groovy code but read like a specialized language. The key insight is that Groovy code like this:
DSL Example
// DSL backing classes
class RouteBuilder {
List routeList = []
def get(String path, Closure handler) { routeList << "GET ${path}"; this }
def post(String path, Closure handler) { routeList << "POST ${path}"; this }
}
class ServerConfig {
int portNum; String hostName; RouteBuilder rb = new RouteBuilder()
def port(int p) { portNum = p }
def host(String h) { hostName = h }
def routes(@DelegatesTo(RouteBuilder) Closure block) {
block.delegate = rb; block.resolveStrategy = Closure.DELEGATE_FIRST; block()
}
}
def listUsers() { 'listing users' }
def createUser() { 'creating user' }
def server(@DelegatesTo(ServerConfig) Closure block) {
def cfg = new ServerConfig()
block.delegate = cfg; block.resolveStrategy = Closure.DELEGATE_FIRST; block()
println "Server on ${cfg.hostName}:${cfg.portNum}"
cfg.rb.routeList.each { println " Route: $it" }
}
// This is valid Groovy code - using our DSL
server {
port 8080
host 'localhost'
routes {
get '/users', { listUsers() }
post '/users', { createUser() }
}
}
…is just method calls with closure arguments. The magic is in how Groovy lets you omit parentheses, use closures as blocks, and redirect method calls through delegates.
How Groovy Enables DSLs
Several Groovy features combine to make DSL creation natural:
- Optional parentheses:
println "hello"instead ofprintln("hello") - Optional semicolons: Cleaner, more readable code
- Closures as last argument:
list.each { println it }instead of passing a Closure object - Closure delegates: Control where unresolved method calls go inside a closure
- Operator overloading: Custom behavior for
+,<<,[], etc. - methodMissing and propertyMissing: Catch and handle undefined calls dynamically
- Named parameters:
new Person(name: 'Alice', age: 30)
The combination of these features means you can create APIs that look nothing like traditional programming — they look like configuration files, markup languages, or plain English instructions.
The following diagram illustrates the step-by-step process of how Groovy DSLs work via closure delegation:
Closure Delegate Strategy
The delegate strategy is the cornerstone of Groovy DSL design. Every closure has three objects it can resolve method calls against: this (the enclosing class), owner (the enclosing object), and delegate (a configurable target). By setting a closure’s delegate, you control where method calls inside that closure get routed.
Delegate Strategy Explained
class ServerConfig {
int port = 80
String host = 'localhost'
boolean ssl = false
void port(int p) { this.port = p }
void host(String h) { this.host = h }
void enableSsl() { this.ssl = true }
String toString() { "${ssl ? 'https' : 'http'}://${host}:${port}" }
}
// DSL method that sets up the delegate
def server(@DelegatesTo(ServerConfig) Closure config) {
def serverConfig = new ServerConfig()
config.delegate = serverConfig
config.resolveStrategy = Closure.DELEGATE_FIRST
config()
return serverConfig
}
// Use the DSL
def myServer = server {
host 'api.example.com'
port 8443
enableSsl()
}
println "Server: ${myServer}"
// Another configuration
def devServer = server {
host 'localhost'
port 3000
}
println "Dev server: ${devServer}"
Output
Server: https://api.example.com:8443 Dev server: http://localhost:3000
The Closure.DELEGATE_FIRST resolve strategy tells Groovy to check the delegate object before the enclosing class when resolving method calls. So when you write host 'api.example.com' inside the closure, Groovy looks for a host() method on the delegate (ServerConfig) first. This is how every Groovy DSL works under the hood — including Gradle.
10 Practical DSL and Builder Examples
Let us work through 10 examples that demonstrate Groovy’s built-in builders and custom DSL techniques.
Example 1: MarkupBuilder for HTML
What we’re doing: Using Groovy’s built-in MarkupBuilder to generate HTML with a clean, tree-structured DSL.
Example 1: MarkupBuilder for HTML
import groovy.xml.MarkupBuilder
def sw = new StringWriter()
def html = new MarkupBuilder(sw)
html.html {
head {
title('Groovy DSL Demo')
meta(charset: 'UTF-8')
link(rel: 'stylesheet', href: 'style.css')
}
body {
h1('Welcome to Groovy Builders')
p('This HTML was generated using MarkupBuilder.')
div(class: 'container') {
h2('User List')
table(class: 'users') {
tr {
th('Name')
th('Email')
th('Role')
}
[
[name: 'Alice', email: 'alice@example.com', role: 'Admin'],
[name: 'Bob', email: 'bob@example.com', role: 'User'],
[name: 'Charlie', email: 'charlie@example.com', role: 'User']
].each { user ->
tr {
td(user.name)
td(user.email)
td(user.role)
}
}
}
}
footer {
p('Generated by Groovy MarkupBuilder')
}
}
}
println sw.toString()
Output
<html>
<head>
<title>Groovy DSL Demo</title>
<meta charset='UTF-8' />
<link rel='stylesheet' href='style.css' />
</head>
<body>
<h1>Welcome to Groovy Builders</h1>
<p>This HTML was generated using MarkupBuilder.</p>
<div class='container'>
<h2>User List</h2>
<table class='users'>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
<td>Admin</td>
</tr>
<tr>
<td>Bob</td>
<td>bob@example.com</td>
<td>User</td>
</tr>
<tr>
<td>Charlie</td>
<td>charlie@example.com</td>
<td>User</td>
</tr>
</table>
</div>
<footer>
<p>Generated by Groovy MarkupBuilder</p>
</footer>
</body>
</html>
What happened here: MarkupBuilder uses Groovy’s methodMissing to convert any method call into an XML/HTML tag. Method name becomes the tag, the string argument becomes content, map arguments become attributes, and closure arguments become child elements. The result is HTML code that visually mirrors its output structure. This is the quintessential Groovy builder pattern.
Example 2: MarkupBuilder for XML
What we’re doing: Generating XML documents using the same MarkupBuilder — identical API, different output.
Example 2: MarkupBuilder for XML
import groovy.xml.MarkupBuilder
def sw = new StringWriter()
def xml = new MarkupBuilder(sw)
xml.library {
book(isbn: '978-1-935182-44-3') {
title('Groovy in Action')
author('Dierk Koenig')
year(2015)
price(currency: 'USD', '49.99')
tags {
tag('groovy')
tag('programming')
tag('jvm')
}
}
book(isbn: '978-0-596-15789-2') {
title('Programming Groovy 2')
author('Venkat Subramaniam')
year(2013)
price(currency: 'USD', '39.99')
tags {
tag('groovy')
tag('dynamic')
}
}
}
println sw.toString()
// Count elements using XmlSlurper on the generated XML
def parsed = new XmlSlurper().parseText(sw.toString())
println "\nBooks found: ${parsed.book.size()}"
parsed.book.each { book ->
println " ${book.title} by ${book.author} (${book.@isbn})"
}
Output
<library>
<book isbn='978-1-935182-44-3'>
<title>Groovy in Action</title>
<author>Dierk Koenig</author>
<year>2015</year>
<price currency='USD'>49.99</price>
<tags>
<tag>groovy</tag>
<tag>programming</tag>
<tag>jvm</tag>
</tags>
</book>
<book isbn='978-0-596-15789-2'>
<title>Programming Groovy 2</title>
<author>Venkat Subramaniam</author>
<year>2013</year>
<price currency='USD'>39.99</price>
<tags>
<tag>groovy</tag>
<tag>dynamic</tag>
</tags>
</book>
</library>
Books found: 2
Groovy in Action by Dierk Koenig (978-1-935182-44-3)
Programming Groovy 2 by Venkat Subramaniam (978-0-596-15789-2)
What happened here: The same MarkupBuilder that generates HTML also generates XML. Notice how attributes are passed as named parameters (like isbn: '...') and content is passed as a positional argument or a closure. We also demonstrated parsing the generated XML back with XmlSlurper, showing the round-trip workflow.
Example 3: StreamingMarkupBuilder for Large Documents
What we’re doing: Using StreamingMarkupBuilder for memory-efficient XML generation and namespace support.
Example 3: StreamingMarkupBuilder
import groovy.xml.StreamingMarkupBuilder
import groovy.xml.XmlUtil
def builder = new StreamingMarkupBuilder()
builder.encoding = 'UTF-8'
def markup = builder.bind {
mkp.xmlDeclaration()
mkp.declareNamespace(atom: 'http://www.w3.org/2005/Atom')
atom.feed {
atom.title('TechnoScripts Blog Feed')
atom.link(href: 'https://technoscripts.com', rel: 'alternate')
atom.updated('2026-03-08T12:00:00Z')
atom.author {
atom.name('TechnoScripts')
}
['Groovy DSL Guide', 'Builder Patterns', 'Closure Delegates'].each { postTitle ->
atom.entry {
atom.title(postTitle)
atom.link(href: "https://technoscripts.com/${postTitle.toLowerCase().replaceAll(' ', '-')}/")
atom.summary("Learn about ${postTitle} in Groovy")
}
}
}
}
// Pretty print the output
println XmlUtil.serialize(markup.toString())
Output
<?xml version="1.0" encoding="UTF-8"?>
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom">
<atom:title>TechnoScripts Blog Feed</atom:title>
<atom:link href="https://technoscripts.com" rel="alternate"/>
<atom:updated>2026-03-08T12:00:00Z</atom:updated>
<atom:author>
<atom:name>TechnoScripts</atom:name>
</atom:author>
<atom:entry>
<atom:title>Groovy DSL Guide</atom:title>
<atom:link href="https://technoscripts.com/groovy-dsl-guide/"/>
<atom:summary>Learn about Groovy DSL Guide in Groovy</atom:summary>
</atom:entry>
<atom:entry>
<atom:title>Builder Patterns</atom:title>
<atom:link href="https://technoscripts.com/builder-patterns/"/>
<atom:summary>Learn about Builder Patterns in Groovy</atom:summary>
</atom:entry>
<atom:entry>
<atom:title>Closure Delegates</atom:title>
<atom:link href="https://technoscripts.com/closure-delegates/"/>
<atom:summary>Learn about Closure Delegates in Groovy</atom:summary>
</atom:entry>
</atom:feed>
What happened here: StreamingMarkupBuilder generates XML lazily — it does not build the entire document tree in memory. This makes it ideal for large documents. The mkp object gives you access to special operations like XML declarations and namespace declarations. Notice how the Atom namespace prefix is used naturally as method calls.
Example 4: CliBuilder for Command-Line Arguments
What we’re doing: Using CliBuilder to parse command-line arguments with a clean DSL syntax.
Example 4: CliBuilder for CLI Parsing
import groovy.cli.picocli.CliBuilder
def cli = new CliBuilder(usage: 'dataprocessor [options] <input-file>')
cli.h(longOpt: 'help', 'Show usage information')
cli.v(longOpt: 'verbose', 'Enable verbose output')
cli.o(longOpt: 'output', args: 1, argName: 'file', 'Output file path')
cli.f(longOpt: 'format', args: 1, argName: 'fmt', 'Output format (csv|json|xml)')
cli.n(longOpt: 'limit', args: 1, argName: 'count', type: Integer, 'Limit number of records')
cli.d(longOpt: 'delimiter', args: 1, argName: 'char', 'CSV delimiter character')
// Simulate command-line arguments
def args = ['-v', '--output', 'result.json', '-f', 'json', '-n', '100', 'input.csv']
def options = cli.parse(args)
if (!options) {
return
}
if (options.h) {
cli.usage()
return
}
println "=== Parsed Options ==="
println "Verbose: ${options.v}"
println "Output file: ${options.o}"
println "Format: ${options.f}"
println "Limit: ${options.n}"
println "Remaining args: ${options.arguments()}"
// Simulate processing
println "\n=== Processing ==="
println "Reading from: ${options.arguments()[0]}"
println "Writing ${options.f.toUpperCase()} to: ${options.o}"
println "Processing up to ${options.n} records..."
if (options.v) {
println "[VERBOSE] Detailed logging enabled"
}
println "Done!"
Output
=== Parsed Options === Verbose: true Output file: result.json Format: json Limit: 100 Remaining args: [input.csv] === Processing === Reading from: input.csv Writing JSON to: result.json Processing up to 100 records... [VERBOSE] Detailed logging enabled Done!
What happened here: CliBuilder is a Groovy DSL for parsing command-line arguments. Each method call defines an option: the method name is the short flag, longOpt is the full name, args specifies how many values the option takes, and the last string is the help text. The result is a clean API that replaces the verbose Apache Commons CLI setup Java typically requires.
Example 5: ObjectGraphBuilder for Object Trees
What we’re doing: Using ObjectGraphBuilder to construct complex object hierarchies with a tree-style DSL.
Example 5: ObjectGraphBuilder
// Define domain classes
class Company {
String name
String industry
List departments = []
String toString() { "Company(${name}, ${industry}, ${departments.size()} depts)" }
}
class Department {
String name
String floor
List employees = []
String toString() { "Dept(${name}, floor ${floor}, ${employees.size()} employees)" }
}
class Employee {
String name
String role
int salary
String toString() { "Employee(${name}, ${role}, \$${salary})" }
}
// Build object graph with DSL
def builder = new ObjectGraphBuilder()
builder.classLoader = this.class.classLoader
builder.classNameResolver = { String name -> name[0].toUpperCase() + name[1..-1] }
def company = builder.company(name: 'TechCorp', industry: 'Software') {
department(name: 'Engineering', floor: '3') {
employee(name: 'Alice', role: 'Lead Developer', salary: 120000)
employee(name: 'Bob', role: 'Developer', salary: 95000)
employee(name: 'Charlie', role: 'Junior Developer', salary: 70000)
}
department(name: 'Marketing', floor: '2') {
employee(name: 'Diana', role: 'Marketing Manager', salary: 105000)
employee(name: 'Edward', role: 'Content Writer', salary: 65000)
}
}
println company
company.departments.each { dept ->
println " ${dept}"
dept.employees.each { emp ->
println " ${emp}"
}
}
println "\nTotal employees: ${company.departments.sum { it.employees.size() }}"
def avgSalary = company.departments.collectMany { it.employees }.collect { it.salary }.average()
println "Average salary: \$${avgSalary as int}"
Output
Company(TechCorp, Software, 2 depts)
Dept(Engineering, floor 3, 3 employees)
Employee(Alice, Lead Developer, $120000)
Employee(Bob, Developer, $95000)
Employee(Charlie, Junior Developer, $70000)
Dept(Marketing, floor 2, 2 employees)
Employee(Diana, Marketing Manager, $105000)
Employee(Edward, Content Writer, $65000)
Total employees: 5
Average salary: $91000
What happened here: ObjectGraphBuilder automatically maps the DSL structure to your domain classes. It resolves class names from method names, sets properties from named parameters, and handles parent-child relationships (adding children to collection properties). This eliminates all the manual construction and wiring code you would write in Java.
Example 6: JsonBuilder for JSON Output
What we’re doing: Using Groovy’s JsonBuilder to generate JSON with the same builder DSL style.
Example 6: JsonBuilder
import groovy.json.JsonBuilder
import groovy.json.JsonOutput
def builder = new JsonBuilder()
builder {
application {
name 'Groovy DSL Demo'
version '2.0'
features 'builders', 'closures', 'traits'
}
database {
host 'localhost'
port 5432
name 'appdb'
pool {
min 5
max 20
timeout 30000
}
}
endpoints([
[path: '/api/users', method: 'GET', auth: true],
[path: '/api/login', method: 'POST', auth: false],
[path: '/api/health', method: 'GET', auth: false]
])
}
println JsonOutput.prettyPrint(builder.toString())
// StreamingJsonBuilder for large outputs
println "\n=== Streaming JSON ==="
def sw = new StringWriter()
def sjb = new groovy.json.StreamingJsonBuilder(sw)
sjb {
users([
[id: 1, name: 'Alice', active: true],
[id: 2, name: 'Bob', active: false]
])
}
println JsonOutput.prettyPrint(sw.toString())
Output
{
"application": {
"name": "Groovy DSL Demo",
"version": "2.0",
"features": [
"builders",
"closures",
"traits"
]
},
"database": {
"host": "localhost",
"port": 5432,
"name": "appdb",
"pool": {
"min": 5,
"max": 20,
"timeout": 30000
}
},
"endpoints": [
{
"path": "/api/users",
"method": "GET",
"auth": true
},
{
"path": "/api/login",
"method": "POST",
"auth": false
},
{
"path": "/api/health",
"method": "GET",
"auth": false
}
]
}
=== Streaming JSON ===
{
"users": [
{
"id": 1,
"name": "Alice",
"active": true
},
{
"id": 2,
"name": "Bob",
"active": false
}
]
}
What happened here: JsonBuilder follows the same pattern as MarkupBuilder — method names become keys, arguments become values, and closures create nested objects. Groovy also provides StreamingJsonBuilder for memory-efficient output. This is the standard way to generate JSON in Groovy scripts, and it is dramatically cleaner than concatenating strings or building maps manually.
Example 7: Custom DSL – Configuration Builder
What we’re doing: Building a complete custom DSL for application configuration from scratch.
Example 7: Custom Configuration DSL
class ConfigDSL {
Map config = [:]
String currentSection = ''
void section(String name, Closure block) {
config[name] = [:]
currentSection = name
block.delegate = new SectionBuilder(config[name])
block.resolveStrategy = Closure.DELEGATE_FIRST
block()
}
static Map build(@DelegatesTo(ConfigDSL) Closure block) {
def dsl = new ConfigDSL()
block.delegate = dsl
block.resolveStrategy = Closure.DELEGATE_FIRST
block()
dsl.config
}
}
class SectionBuilder {
Map data
SectionBuilder(Map data) { this.data = data }
def methodMissing(String name, args) {
if (args.length == 1 && args[0] instanceof Closure) {
data[name] = [:]
def nested = new SectionBuilder(data[name])
args[0].delegate = nested
args[0].resolveStrategy = Closure.DELEGATE_FIRST
args[0]()
} else {
data[name] = args.length == 1 ? args[0] : args.toList()
}
}
}
// Use the DSL
def appConfig = ConfigDSL.build {
section('server') {
host 'localhost'
port 8080
ssl false
cors {
enabled true
origins 'https://example.com', 'https://api.example.com'
}
}
section('database') {
driver 'postgresql'
url 'jdbc:postgresql://localhost:5432/mydb'
username 'admin'
pool {
minSize 5
maxSize 20
idleTimeout 300
}
}
section('logging') {
level 'INFO'
file '/var/log/app.log'
rotate true
}
}
// Display configuration
appConfig.each { section, values ->
println "[${section}]"
printConfig(values, ' ')
}
def printConfig(Map map, String indent) {
map.each { key, value ->
if (value instanceof Map) {
println "${indent}${key}:"
printConfig(value, indent + ' ')
} else {
println "${indent}${key} = ${value}"
}
}
}
Output
[server]
host = localhost
port = 8080
ssl = false
cors:
enabled = true
origins = [https://example.com, https://api.example.com]
[database]
driver = postgresql
url = jdbc:postgresql://localhost:5432/mydb
username = admin
pool:
minSize = 5
maxSize = 20
idleTimeout = 300
[logging]
level = INFO
file = /var/log/app.log
rotate = true
What happened here: We built a configuration DSL from scratch using methodMissing and closure delegates. When you write host 'localhost' inside a section, Groovy calls methodMissing('host', ['localhost']) on the SectionBuilder, which stores the key-value pair. When a closure argument is passed, it creates a nested section. This is the exact technique Gradle uses for its build DSL.
Example 8: Custom DSL – SQL Query Builder
What we’re doing: Creating a DSL that builds SQL queries in a type-safe, readable way.
Example 8: SQL Query Builder DSL
class QueryBuilder {
String tableName = ''
List<String> columns = ['*']
List<String> conditions = []
String orderColumn = ''
String orderDirection = 'ASC'
int limitValue = -1
int offsetValue = 0
List<String> joinClauses = []
QueryBuilder from(String table) { this.tableName = table; this }
QueryBuilder select(String... cols) { this.columns = cols.toList(); this }
QueryBuilder where(String condition) { this.conditions << condition; this }
QueryBuilder orderBy(String column, String direction = 'ASC') {
this.orderColumn = column
this.orderDirection = direction
this
}
QueryBuilder limit(int n) { this.limitValue = n; this }
QueryBuilder offset(int n) { this.offsetValue = n; this }
QueryBuilder join(String table, String on) {
joinClauses << "JOIN ${table} ON ${on}"
this
}
QueryBuilder leftJoin(String table, String on) {
joinClauses << "LEFT JOIN ${table} ON ${on}"
this
}
String build() {
def sql = new StringBuilder("SELECT ${columns.join(', ')} FROM ${tableName}")
joinClauses.each { sql.append("\n ${it}") }
if (conditions) sql.append("\n WHERE ${conditions.join(' AND ')}")
if (orderColumn) sql.append("\n ORDER BY ${orderColumn} ${orderDirection}")
if (limitValue > 0) sql.append("\n LIMIT ${limitValue}")
if (offsetValue > 0) sql.append("\n OFFSET ${offsetValue}")
sql.toString()
}
static String query(@DelegatesTo(QueryBuilder) Closure block) {
def qb = new QueryBuilder()
block.delegate = qb
block.resolveStrategy = Closure.DELEGATE_FIRST
block()
qb.build()
}
}
// Simple query
println QueryBuilder.query {
select 'name', 'email', 'role'
from 'users'
where "active = true"
where "role = 'admin'"
orderBy 'name'
}
println ""
// Complex query with joins
println QueryBuilder.query {
select 'u.name', 'u.email', 'o.total', 'o.date'
from 'users u'
join 'orders o', 'u.id = o.user_id'
leftJoin 'addresses a', 'u.id = a.user_id'
where "o.total > 100"
where "o.date > '2026-01-01'"
orderBy 'o.total', 'DESC'
limit 20
offset 40
}
Output
SELECT name, email, role FROM users WHERE active = true AND role = 'admin' ORDER BY name ASC SELECT u.name, u.email, o.total, o.date FROM users u JOIN orders o ON u.id = o.user_id LEFT JOIN addresses a ON u.id = a.user_id WHERE o.total > 100 AND o.date > '2026-01-01' ORDER BY o.total DESC LIMIT 20 OFFSET 40
What happened here: This SQL query builder DSL demonstrates fluent method chaining combined with closure delegates. Each method returns this for chaining, and the static query() method provides the DSL entry point. The resulting queries are readable, composable, and free from string concatenation errors. This is the same approach used by GORM (Groovy Object Relational Mapping) in Grails.
Example 9: Custom DSL – Test Specification
What we’re doing: Building a mini testing DSL similar to Spock’s given-when-then structure.
Example 9: Testing DSL
class TestSuite {
String name
List results = []
int passed = 0
int failed = 0
static void describe(String name, @DelegatesTo(TestSuite) Closure block) {
def suite = new TestSuite(name: name)
block.delegate = suite
block.resolveStrategy = Closure.DELEGATE_FIRST
block(suite)
suite.printResults()
}
void it(String description, Closure test) {
try {
test.delegate = this
test.resolveStrategy = Closure.DELEGATE_FIRST
test()
results << [description: description, status: 'PASSED']
passed++
} catch (AssertionError | Exception e) {
results << [description: description, status: 'FAILED', error: e.message]
failed++
}
}
Expectation expect(actual) {
new Expectation(actual: actual)
}
void printResults() {
println "Suite: ${name}"
println "=" * 50
results.each { r ->
def icon = r.status == 'PASSED' ? '[PASS]' : '[FAIL]'
println " ${icon} ${r.description}"
if (r.error) println " Error: ${r.error}"
}
println "-" * 50
println "Results: ${passed} passed, ${failed} failed, ${passed + failed} total\n"
}
}
class Expectation {
def actual
void toBe(expected) {
assert actual == expected : "Expected ${expected} but got ${actual}"
}
void toContain(item) {
assert actual.contains(item) : "Expected ${actual} to contain ${item}"
}
void toBeGreaterThan(value) {
assert actual > value : "Expected ${actual} to be greater than ${value}"
}
}
def expect(actual) { new Expectation(actual: actual) }
// Use the DSL
TestSuite.describe('Calculator Tests') { suite ->
suite.it('should add two numbers') {
expect(2 + 3).toBe(5)
}
suite.it('should multiply numbers') {
expect(4 * 5).toBe(20)
}
suite.it('should handle string operations') {
expect('Hello' + ' World').toBe('Hello World')
}
suite.it('should find items in a list') {
expect([1, 2, 3, 4, 5]).toContain(3)
}
suite.it('should compare values') {
expect(10).toBeGreaterThan(5)
}
suite.it('should catch failures') {
expect(2 + 2).toBe(5)
}
}
Output
Suite: Calculator Tests
==================================================
[PASS] should add two numbers
[PASS] should multiply numbers
[PASS] should handle string operations
[PASS] should find items in a list
[PASS] should compare values
[FAIL] should catch failures
Error: Expected 5 but got 4. Expression: (actual == expected). Values: actual = 4, expected = 5
--------------------------------------------------
Results: 5 passed, 1 failed, 6 total
What happened here: We built a mini testing framework with just two classes. The describe block sets up a test suite, it defines individual tests, and expect().toBe() provides assertions. This is a simplified version of what Spock does — and it demonstrates how DSLs can make test code read like plain English specifications.
Example 10: Custom DSL – Pipeline Builder
What we’re doing: Creating a data processing pipeline DSL similar to Jenkins Pipeline or ETL tools.
Example 10: Pipeline Builder DSL
class Pipeline {
String name
List stages = []
static def build(String name, @DelegatesTo(Pipeline) Closure block) {
def pipeline = new Pipeline(name: name)
block.delegate = pipeline
block.resolveStrategy = Closure.DELEGATE_FIRST
block()
pipeline
}
void stage(String name, Closure action) {
stages << [name: name, action: action]
}
def execute(data) {
println "Pipeline: ${name}"
println "=" * 50
def result = data
stages.eachWithIndex { stage, index ->
print " Stage ${index + 1}: ${stage.name}... "
try {
result = stage.action(result)
println "OK"
} catch (Exception e) {
println "FAILED: ${e.message}"
throw e
}
}
println "=" * 50
println "Pipeline completed successfully!"
result
}
}
// Define a data processing pipeline
def etl = Pipeline.build('Customer Data ETL') {
stage('Extract') { data ->
// Simulate extracting raw data
[
[name: ' alice smith ', email: 'ALICE@EXAMPLE.COM', age: '30', active: 'true'],
[name: ' bob jones ', email: 'BOB@EXAMPLE.COM', age: '25', active: 'false'],
[name: ' charlie brown ', email: 'CHARLIE@EXAMPLE.COM', age: '35', active: 'true'],
[name: ' diana prince ', email: 'diana@EXAMPLE.COM', age: '28', active: 'true']
]
}
stage('Clean') { data ->
data.collect { record ->
record.collectEntries { key, value ->
[key, value.toString().trim()]
}
}
}
stage('Transform') { data ->
data.collect { record ->
[
name: record.name.split(' ').collect { it.capitalize() }.join(' '),
email: record.email.toLowerCase(),
age: record.age as int,
active: record.active.toBoolean()
]
}
}
stage('Filter') { data ->
data.findAll { it.active && it.age >= 25 }
}
stage('Load') { data ->
println ""
data.each { record ->
println " Loaded: ${record.name} (${record.email}), age ${record.age}"
}
data
}
}
// Execute the pipeline
def result = etl.execute(null)
println "\nProcessed ${result.size()} records"
Output
Pipeline: Customer Data ETL
==================================================
Stage 1: Extract... OK
Stage 2: Clean... OK
Stage 3: Transform... OK
Stage 4: Filter... OK
Stage 5: Load...
Loaded: Alice Smith (alice@example.com), age 30
Loaded: Charlie Brown (charlie@example.com), age 35
Loaded: Diana Prince (diana@example.com), age 28
OK
==================================================
Pipeline completed successfully!
Processed 3 records
What happened here: This pipeline DSL chains processing stages together, passing data through each stage. Each stage takes a name and a closure that receives the previous stage’s output. The pattern is identical to Jenkins Pipeline, Apache Beam, or any ETL tool — but written in pure Groovy. Adding a new stage is just adding another stage('Name') { ... } block.
Bonus Example 11: DSL with Operator Overloading
What we’re doing: Using Groovy’s operator overloading to create a natural-feeling DSL for building expressions.
Bonus: Operator Overloading DSL
class Duration {
int value
String unit
Duration(int value, String unit) {
this.value = value
this.unit = unit
}
Duration plus(Duration other) {
// Convert everything to seconds for simplicity
def thisSeconds = toSeconds()
def otherSeconds = other.toSeconds()
new Duration(thisSeconds + otherSeconds, 'seconds')
}
Duration multiply(int factor) {
new Duration(value * factor, unit)
}
int toSeconds() {
switch (unit) {
case 'seconds': return value
case 'minutes': return value * 60
case 'hours': return value * 3600
case 'days': return value * 86400
default: return value
}
}
String toString() {
if (unit == 'seconds' && value >= 3600) {
def hours = value.intdiv(3600)
def mins = (value % 3600).intdiv(60)
def secs = value % 60
return "${hours}h ${mins}m ${secs}s"
}
"${value} ${unit}"
}
}
// Add DSL methods to Integer
Integer.metaClass.getSeconds = { -> new Duration(delegate, 'seconds') }
Integer.metaClass.getMinutes = { -> new Duration(delegate, 'minutes') }
Integer.metaClass.getHours = { -> new Duration(delegate, 'hours') }
Integer.metaClass.getDays = { -> new Duration(delegate, 'days') }
// Natural-feeling duration DSL
def timeout = 30.seconds
println "Timeout: ${timeout}"
def lunchBreak = 45.minutes
println "Lunch break: ${lunchBreak}"
def workday = 8.hours
println "Workday: ${workday}"
def sprint = 14.days
println "Sprint: ${sprint}"
// Arithmetic with durations
def meeting = 1.hours + 30.minutes
println "Meeting duration: ${meeting}"
def doubleShift = 8.hours * 2
println "Double shift: ${doubleShift}"
def totalBreaks = 15.minutes + 45.minutes + 15.minutes
println "Total breaks: ${totalBreaks}"
Output
Timeout: 30 seconds Lunch break: 45 minutes Workday: 8 hours Sprint: 14 days Meeting duration: 1h 30m 0s Double shift: 16 hours Total breaks: 0h 75m 0s
What happened here: By adding property getters to Integer via metaprogramming and overloading plus() and multiply() on the Duration class, we created a DSL where 30.minutes reads like natural language. This technique is used in Spock for timeouts, in Grails for configuration, and in many Groovy-based tools where readability matters.
Creating Custom Builders
Groovy provides the BuilderSupport and FactoryBuilderSupport base classes for creating custom builders. Here is a summary of when to use each approach:
| Approach | Best For | Complexity |
|---|---|---|
| Closure delegates | Simple configuration DSLs | Low |
| methodMissing | Open-ended DSLs with unknown keys | Medium |
| BuilderSupport | Tree-structured outputs | Medium |
| FactoryBuilderSupport | Complex builders with multiple node types | High |
| AST transformations | Compile-time DSL enforcement | Very High |
For most DSL needs, closure delegates combined with methodMissing give you 90% of the power with 10% of the complexity. Only reach for BuilderSupport or FactoryBuilderSupport when you need framework-level builder infrastructure.
Real-World DSLs Built with Groovy
Groovy DSLs power some of the most popular tools in the JVM ecosystem:
- Gradle: Build automation —
build.gradleis a Groovy DSL - Spock: Testing framework — given/when/then blocks are a DSL for specifications
- Jenkins Pipeline: CI/CD — Jenkinsfile uses a Groovy DSL for build pipelines
- Grails: Web framework — URL mappings, GORM criteria, and configuration are DSLs
- Ratpack: HTTP toolkit — handler chains use a Groovy DSL
- Spring Security DSL: Security configuration in Groovy-based Spring apps
All of these use the same techniques we covered in this tutorial: closure delegates, methodMissing, and builder classes. When you understand Groovy DSLs, you understand how these tools work under the hood.
Best Practices
DO:
- Use
@DelegatesToannotations to enable IDE autocompletion and type checking in DSLs - Set
resolveStrategy = Closure.DELEGATE_FIRSTto prevent accidental method resolution on the wrong object - Provide clear error messages when DSL usage is incorrect
- Start simple with closure delegates and add complexity only when needed
- Document your DSL with examples — the syntax is not always obvious to newcomers
DON’T:
- Create a DSL when a simple API would do — DSLs add learning overhead
- Overuse
methodMissing— it makes debugging harder and hides errors - Forget that DSL code is still Groovy code — users can inject arbitrary logic
- Ignore security when evaluating user-provided DSL scripts (use
SecureASTCustomizer) - Build deeply nested DSLs that are hard to read — aim for 2-3 levels of nesting maximum
Conclusion
We covered Groovy DSLs and the Builder pattern from the ground up — starting with MarkupBuilder for HTML and XML, JsonBuilder for JSON, CliBuilder for command-line parsing, and ObjectGraphBuilder for object trees. We then built custom DSLs for configuration, SQL queries, testing, and data pipelines using closure delegates, methodMissing, and operator overloading.
The key insight is that every Groovy DSL is built on the same foundation: closures with configurable delegates. When you write server { port 8080 }, Groovy is just calling a method named port on the closure’s delegate object. Understanding this mechanism lets you read any Groovy DSL and build your own.
For related topics, check out our Groovy Design Patterns tutorial where builders and strategies are explored further, and our Groovy Concurrency tutorial for async DSL patterns.
Summary
- Groovy DSLs are valid Groovy code that reads like a domain-specific language
- Closure delegate strategy (
DELEGATE_FIRST) is the foundation of every Groovy DSL MarkupBuilder,JsonBuilder,CliBuilder, andObjectGraphBuilderare built-in DSL toolsmethodMissingenables open-ended DSLs where keys are not known in advance- Real-world tools like Gradle, Spock, and Jenkins Pipeline are all Groovy DSLs
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Concurrency – Threads, GPars, and Async
Frequently Asked Questions
What is a Groovy DSL and how is it different from a regular API?
A Groovy DSL (Domain-Specific Language) is Groovy code structured to read like a specialized language for a specific task. Unlike a regular API where you call methods with parentheses and dots, a DSL uses closures, optional parentheses, and delegate strategies to create a natural, readable syntax. For example, server { port 8080 } is a DSL — it looks like configuration but is executable Groovy code.
What is the closure delegate strategy in Groovy?
The delegate strategy controls where Groovy looks for methods called inside a closure. With Closure.DELEGATE_FIRST, Groovy checks the delegate object before the enclosing class. This is the key mechanism behind DSLs: you set a closure’s delegate to a domain object, and method calls inside the closure get routed to that object. This is how Gradle build files work under the hood.
What is the difference between MarkupBuilder and StreamingMarkupBuilder?
MarkupBuilder builds the entire XML/HTML document in memory and writes it all at once. StreamingMarkupBuilder generates output lazily, writing it as it goes without storing the full document tree. Use MarkupBuilder for small to medium documents and StreamingMarkupBuilder for large documents or when you need namespace support and XML declarations.
How does Groovy’s CliBuilder work for command-line parsing?
CliBuilder uses a DSL where each method call defines a command-line option. The method name becomes the short flag (e.g., -v), longOpt sets the full name (e.g., --verbose), and args specifies how many values the option accepts. After calling cli.parse(args), you access options as properties on the returned object. It replaces the verbose Apache Commons CLI setup that Java typically requires.
How do I create my own custom DSL in Groovy?
Start by creating a class that holds your domain data. Write a static method that accepts a closure, creates an instance of your class, sets it as the closure’s delegate with DELEGATE_FIRST strategy, and executes the closure. Methods on your class become DSL keywords. For open-ended DSLs, implement methodMissing to handle unknown method names dynamically. Use @DelegatesTo for IDE support.
Related Posts
Previous in Series: Groovy Design Patterns – Singleton, Strategy, Observer
Next in Series: Groovy Concurrency – Threads, GPars, and Async
Related Topics You Might Like:
- Groovy Design Patterns – Singleton, Strategy, Observer
- Groovy Concurrency – Threads, GPars, and Async
- Groovy Grape – Dependency Management in Scripts
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment