Groovy Command Chains — Fluent DSL and Builder Syntax
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