Groovy DSL and Builder Pattern – Create Domain-Specific Languages with 10 Examples

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.

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:

TypeDescriptionExamples
External DSLSeparate language with its own parserSQL, HTML, CSS, Regular Expressions
Internal DSLEmbedded within a host languageGradle 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 of println("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:

User writes DSL:

server {
host ‘localhost’
port 8080
routes {
get ‘/api’
}
}

How it works:

1. server() method
receives a Closure

2. closure.delegate =
new ServerConfig()

3. closure.resolveStrategy =
Closure.DELEGATE_FIRST

4. closure.call()
executes the body

5. host, port, routes
resolve on ServerConfig
(the delegate)

6. Nested closure (routes)
gets its own delegate
(RouteConfig)

Groovy DSL – Builder Pattern 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:

ApproachBest ForComplexity
Closure delegatesSimple configuration DSLsLow
methodMissingOpen-ended DSLs with unknown keysMedium
BuilderSupportTree-structured outputsMedium
FactoryBuilderSupportComplex builders with multiple node typesHigh
AST transformationsCompile-time DSL enforcementVery 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.gradle is 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 @DelegatesTo annotations to enable IDE autocompletion and type checking in DSLs
  • Set resolveStrategy = Closure.DELEGATE_FIRST to 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, and ObjectGraphBuilder are built-in DSL tools
  • methodMissing enables 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.

Previous in Series: Groovy Design Patterns – Singleton, Strategy, Observer

Next in Series: Groovy Concurrency – Threads, GPars, and Async

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *