Skip to content

Groovy Command Chains — Fluent DSL and Builder Syntax

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Groovy Command Chains. We cover key concepts, practical examples, and best practices to help you master this topic.

Groovy command chains eliminate punctuation between method calls, enabling natural-language DSLs and fluent builder APIs without dots or parentheses for cleaner, more readable code.

What You'll Learn

  • Command chain syntax rules
  • Building fluent APIs
  • Chaining with closures
  • Real-world DSL examples

Why It Matters

Command chains make Groovy the best JVM language for DSLs. Gradle build scripts and Jenkins pipelines are written entirely using command chains. Durga Antivirus Pro uses them for its rule definition DSL.

Real-World Use

Build configuration (Gradle), CI/CD pipelines (Jenkins), testing (Spock), REST API clients, and configuration DSLs where readability is paramount.

flowchart LR
    A["Command Chains"] --> B["No Parentheses"]
    A --> C["No Dots"]
    B --> D["Method Space Args"]
    C --> E["Chained Calls"]
    D --> F["DSL"]
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af

What Are Command Chains?

A command chain lets you write method arg1 arg2 arg3 without parentheses or dots:

// Normal Groovy
object.method(arg1, arg2)

// Command chain
object method arg1, arg2

Multiple calls are chained with spaces:

// Without chain
builder.foo("bar").baz("qux")

// With chain
builder foo "bar" baz "qux"

Basic Examples

class Robot {
    void move(String direction, int steps) {
        println "Moving $direction $steps steps"
    }
    void speak(String message) {
        println "Says: $message"
    }
    void beep(int times) {
        println "Beep! " * times
    }
}

def robot = new Robot()

// Command chain
robot move "forward", 5 speak "Hello" beep 3
// Moving forward 5 steps
// Says: Hello
// Beep! Beep! Beep!

Command Chains with Closures

class Configurator {
    def settings = [:]

    void server(String name, Closure config) {
        settings[name] = [:]
        config.delegate = settings[name]
        config()
    }

    void host(String h) { settings['host'] = h }
    void port(int p) { settings['port'] = p }
}

def config = new Configurator()

// DSL-style
config server "api" {
    host "example.com"
    port 8080
}

println config.settings
// [api:[host:example.com, port:8080], host:example.com, port:8080]

Gradle-Style DSL

class Dependencies {
    private deps = []

    void implementation(String dep) { deps << dep }
    void testImplementation(String dep) { deps << "test: $dep" }
    void api(String dep) { deps << dep }

    String toString() { deps.join('\n') }
}

class BuildScript {
    void dependencies(Closure config) {
        def deps = new Dependencies()
        config.delegate = deps
        config()
        println deps
    }
}

def build = new BuildScript()
build dependencies {
    implementation 'com.google.guava:guava:32.0'
    testImplementation 'junit:junit:4.13'
    api 'com.google.code.gson:gson:2.10'
}

Jenkins Pipeline Style

class JenkinsPipeline {
    def stages = []

    void stage(String name, Closure block) {
        stages << [name: name, actions: block]
    }

    void sh(String command) {
        println "Executing: $command"
    }

    void echo(String msg) {
        println msg
    }

    void run() {
        stages.each { stage ->
            println "--- Stage: ${stage.name} ---"
            stage.actions.delegate = this
            stage.actions()
        }
    }
}

def pipeline = new JenkinsPipeline()
pipeline stage "Checkout" {
    sh "git clone repo"
} stage "Build" {
    sh "gradle build"
} stage "Test" {
    sh "gradle test"
    echo "Tests passed"
}
pipeline.run()

Named Arguments in Chains

class HttpRequest {
    void get(String url, Map headers) {
        println "GET $url with $headers"
    }

    void post(String url, Map body, Map headers) {
        println "POST $url body=$body headers=$headers"
    }
}

def http = new HttpRequest()

// Named args in chains
http get "https://api.example.com/users", headers: ["Authorization": "Bearer token"]
http post "https://api.example.com/users", body: [name: "Alice"], headers: ["Content-Type": "application/json"]

Chaining with Mixed Syntax

Command chains work best when the receiver object provides all the methods:

class Assertion {
    def actual

    void should(Map matchers) {
        matchers.each { key, value ->
            switch(key) {
                case 'equal': assert actual == value
                case 'contain': assert actual.contains(value)
            }
        }
    }
}

class Spec {
    def given(Object obj) { new Assertion(actual: obj) }
}

def spec = new Spec()
spec given "hello" should equal: "hello", contain: "ell"
println "Assertions passed"

Common Mistakes

1. Missing receiver for first call

// Wrong — no receiver
move "forward", 5

// Right — must have receiver
robot move "forward", 5

2. Ambiguity with closures

Parentheses are needed when a closure argument could be confused with a body block.

3. Mixing chain styles inconsistently

Choose one style (chain or dot) per call chain. Mixing is confusing and error-prone.

4. Forgetting commas between arguments

method a b means method(a, b) — two arguments. method a b c means three calls: method(a).b(c).

5. Long chains reduce readability

Break long chains into named intermediate variables.

Practice Questions

1. What is a command chain in Groovy?

A syntax where method calls are separated by spaces instead of dots, with arguments following the method name without parentheses.

2. How does Groovy resolve a b c d?

It calls a(b).c(d) — method b on a, then method c on the result with argument d.

3. Why are command chains useful for DSLs?

They mimic natural language, removing syntactic noise (dots, parens) that interferes with readability.

4. What happens when a chain has an odd number of tokens?

Groovy parses it as method calls on method calls. a b c means a(b).c().

Challenge: Create a REST API client DSL using command chains.

FAQ

{{< faq question="Do command chains have performance overhead?" >}} No. Command chains are a compile-time syntax transformation. The generated bytecode is identical to dot-notation calls. {{< /faq >}}

{{< faq question="Can command chains be nested?" >}} Yes, through closures within command chain arguments. {{< /faq >}}

{{< faq question="Do command chains work with static compilation?" >}} Yes, with @CompileStatic. The compiler resolves method calls at compile time. {{< /faq >}}

{{< faq question="Are command chains unique to Groovy?" >}} Mostly. Kotlin's with and apply are similar but use different syntax. Groovy's space-separated chains are unique. {{< /faq >}}

{{< faq question="How do I debug command chain resolution?" >}} Use println in methods to trace call order. IntelliJ IDEA also shows resolved calls. {{< /faq >}}

Mini Project

Create a test specification DSL using command chains.

class TestSpec {
    def context(String name, Closure block) {
        println "Context: $name"
        block.delegate = this
        block()
    }

    void it(String behavior, Closure test) {
        print "  It $behavior: "
        try {
            test()
            println "PASS"
        } catch (AssertionError e) {
            println "FAIL — ${e.message}"
        }
    }

    void expect(Object actual, Object expected) {
        assert actual == expected
    }
}

def spec = new TestSpec()
spec context "String operations" {
    it "should reverse strings" {
        expect "hello".reverse(), "olleh"
    }
    it "should check length" {
        expect "hello".length(), 5
    }
}

What's Next

Now that you understand command chains, proceed to operator overloading.

Topic Description Link
Operator overloading Custom operators {{< ref "24-operator-overloading" >}}
Builders Markup builder patterns {{< ref "09-builders" >}}
Spock Testing framework {{< ref "16-spock" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro