Groovy Closure Composition & Chaining – Build Pipelines with 12 Examples

Groovy closure composition with leftShift, rightShift, and chaining. 12 examples covering compose, andThen, pipelines, and functional patterns. Groovy 5.0.6.

“Composition is the essence of programming. We compose functions to build bigger functions, just as we compose words to build sentences.”

Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 19 minutes

If you’ve been following our closures series – starting with the Groovy Closures Complete Guide – you’ve seen closures as standalone blocks of reusable logic. But the real power of closures comes when you start combining them. Take two simple closures, compose them together, and you get a new closure that does both things in sequence. No intermediate variables. No temporary collections. Just clean, declarative data flow.

This is closure composition, and Groovy makes it absurdly easy. The << and >> operators let you snap closures together like LEGO bricks. The output of one closure becomes the input of the next. You can build entire transformation pipelines this way – and the resulting code reads almost like English.

In this post, we’ll explore Groovy closure composition from first principles through 12 tested examples. You’ll learn the composition operators, the difference between compose() and andThen(), and real-world patterns for building composable code. If you’ve already worked through higher-order functions like collect, inject, and groupBy, you’ll see how composition builds on those ideas.

The goal is to shift how you think about closures – not as isolated units, but as composable building blocks that you can mix, match, and chain into complex transformations.

The diagram below shows how the same two closures produce two different results depending on the operator you pick: with rightShift the left closure runs first, while with leftShift the right closure runs first, so the value 5 becomes either 20 or 30:

a rightShift b(andThen)a leftShift b(compose)Input value 5Whichoperator?Left closure runs FIRSTRight closure runs FIRSTdouble: 5 times 2 equals 10addTen: 10 plus 10 equals 20Result 20addTen: 5 plus 10 equals 15double: 15 times 2 equals 30Result 30Groovy Closure Composition: rightShift andThen vs leftShift compose Data Flow Direction

What Is Closure Composition?

Closure composition is the process of combining two or more closures into a single closure. The composed closure passes its input through each component closure in sequence, feeding the output of one into the input of the next.

If you remember math class, function composition works like this: given functions f(x) and g(x), the composition f(g(x)) first applies g to x, then applies f to the result. Groovy’s closure composition works exactly the same way, but with operators that make the direction of data flow explicit.

According to the official Groovy closures documentation, Groovy provides two composition operators and two corresponding methods:

  • << (left shift) – compose: applies the right closure first, then the left
  • >> (right shift) – andThen: applies the left closure first, then the right
  • .leftShift() – method equivalent of <<
  • .rightShift() – method equivalent of >>

The >> operator is generally more intuitive because data flows left-to-right, matching how we naturally read code. But both are useful depending on the situation.

The Composition Operators: << and >>

Let’s be crystal clear about which direction data flows with each operator, because this trips up a lot of people:

Composition Direction

def double_ = { it * 2 }
def addTen = { it + 10 }

// >> (rightShift / andThen): left closure runs FIRST
// Read as: "double, THEN add ten"
def doubleThenAdd = double_ >> addTen
println "5 → double → add10 = ${doubleThenAdd(5)}"   // 5*2=10, 10+10=20

// << (leftShift / compose): right closure runs FIRST
// Read as: "double AFTER adding ten"
def doubleAfterAdd = double_ << addTen
println "5 → add10 → double = ${doubleAfterAdd(5)}"  // 5+10=15, 15*2=30

Output

5 → double → add10 = 20
5 → add10 → double = 30

Think of the arrows as showing which direction the data flows. With >>, data flows to the right – left closure first, then right. With <<, data flows to the left – right closure first, then left. Most Groovy developers prefer >> because left-to-right reading order matches execution order.

compose() vs andThen() Methods

Groovy closures also provide leftShift() and rightShift() methods that work exactly like the operators. These are useful when you’re building compositions programmatically or when you want the code to be more self-documenting:

Method-Based Composition

def trim = { it.toString().trim() }
def upper = { it.toUpperCase() }
def exclaim = { it + "!" }

// Using operators
def shoutOp = trim >> upper >> exclaim
println "Operator:  ${shoutOp('  hello  ')}"

// Using methods - same result
def shoutMethod = trim.rightShift(upper).rightShift(exclaim)
println "Method:    ${shoutMethod('  hello  ')}"

// leftShift - reads right to left
def shoutReverse = exclaim.leftShift(upper).leftShift(trim)
println "Reversed:  ${shoutReverse('  hello  ')}"

Output

Operator:  HELLO!
Method:    HELLO!
Reversed:  HELLO!

All three approaches produce identical results. The >> operator version is the most readable, so use that as your default. The method versions exist mainly for programmatic composition – situations where you’re building a pipeline dynamically from a list of closures.

12 Practical Composition Examples

Let’s work through closure composition from basic to advanced. Each example builds on concepts from our closures guide and higher-order functions tutorial.

Example 1: Simple Two-Closure Composition

What we’re doing: Composing two basic closures and seeing how the output of one becomes the input of the other.

Example 1: Simple Two-Closure Composition

def addOne = { it + 1 }
def multiplyByThree = { it * 3 }

// Compose with >> (andThen): addOne first, then multiply
def addThenMultiply = addOne >> multiplyByThree
println "addThenMultiply(4): ${addThenMultiply(4)}"   // (4+1)*3 = 15

// Compose with << (compose): multiply first from the right
def multiplyThenAdd = addOne << multiplyByThree
println "multiplyThenAdd(4): ${multiplyThenAdd(4)}"   // (4*3)+1 = 13

// Verify by doing it manually
println "Manual add then multiply: ${multiplyByThree(addOne(4))}"   // 15
println "Manual multiply then add: ${addOne(multiplyByThree(4))}"   // 13

// The composed closure is itself a closure - verify
println "Type: ${addThenMultiply.getClass().superclass.simpleName}"
println "Is Closure: ${addThenMultiply instanceof Closure}"

Output

addThenMultiply(4): 15
multiplyThenAdd(4): 13
Manual add then multiply: 15
Manual multiply then add: 13
Type: Closure
Is Closure: true

What happened here: The >> operator composed the two closures left-to-right: first add one, then multiply by three. The << operator reversed the order. Both produce a new Closure object that encapsulates both operations. The manual verification confirms the math is correct – composition just eliminates the nesting.

Example 2: Multi-Step String Transformation Pipeline

What we’re doing: Chaining multiple closures into a string processing pipeline.

Example 2: String Transformation Pipeline

def trim    = { it.trim() }
def lower   = { it.toLowerCase() }
def replace = { it.replaceAll(/\s+/, '-') }
def limit   = { it.take(30) }
def prefix  = { "blog-${it}" }

// Chain all five closures
def slugify = trim >> lower >> replace >> limit >> prefix

// Test with various inputs
def titles = [
    "  Hello World  ",
    "  Groovy Closure Composition and Chaining Guide  ",
    "  10 Tips For Better   Code Quality  ",
    "SHORT"
]

titles.each { title ->
    println "'${title.trim()}'"
    println "  → '${slugify(title)}'"
    println()
}

// Each step in isolation
def input = "  Groovy Is GREAT  "
println "Step by step:"
println "  1. trim:    '${trim(input)}'"
println "  2. lower:   '${lower(trim(input))}'"
println "  3. replace: '${replace(lower(trim(input)))}'"
println "  4. limit:   '${limit(replace(lower(trim(input))))}'"
println "  5. prefix:  '${prefix(limit(replace(lower(trim(input)))))}'"
println "  Composed:   '${slugify(input)}'"

Output

'Hello World'
  → 'blog-hello-world'

'Groovy Closure Composition and Chaining Guide'
  → 'blog-groovy-closure-composition-and'

'10 Tips For Better   Code Quality'
  → 'blog-10-tips-for-better-code-qualit'

'SHORT'
  → 'blog-short'

Step by step:
  1. trim:    'Groovy Is GREAT'
  2. lower:   'groovy is great'
  3. replace: 'groovy-is-great'
  4. limit:   'groovy-is-great'
  5. prefix:  'blog-groovy-is-great'
  Composed:   'blog-groovy-is-great'

What happened here: Five individual closures compose into one slugify closure that processes input through the entire pipeline. Compare the nested call in the step-by-step section – prefix(limit(replace(lower(trim(input))))) – with the composed version slugify(input). The composed version is dramatically more readable, and you can reuse it anywhere without repeating the chain.

Example 3: Composing Closures with Different Types

What we’re doing: Composing closures where the output type of one closure differs from its input type.

Example 3: Type-Changing Composition

// String → Integer → Double → String
def parseAge = { String s -> Integer.parseInt(s.trim()) }
def toDecades = { int age -> age / 10.0 }
def formatDecades = { double d -> String.format("%.1f decades", d) }

def ageToDecades = parseAge >> toDecades >> formatDecades

println ageToDecades("25")
println ageToDecades("  42  ")
println ageToDecades("100")

// String → List → Integer → Boolean
def splitWords = { String s -> s.split(/\s+/) as List }
def countWords = { List words -> words.size() }
def isLongSentence = { int count -> count > 5 }

def isSentenceLong = splitWords >> countWords >> isLongSentence

def sentences = [
    "Hello",
    "Groovy is a great language",
    "The quick brown fox jumps over the lazy dog"
]

sentences.each { s ->
    println "'${s}' → long? ${isSentenceLong(s)}"
}

// Number → String → Number (round-trip)
def toCurrency = { Number n -> String.format("\$%.2f", n) }
def parseCurrency = { String s -> Double.parseDouble(s.replace('$', '')) }

def roundTrip = toCurrency >> parseCurrency
println "Round-trip 42.5: ${roundTrip(42.5)}"
println "Same value: ${roundTrip(42.5) == 42.5}"

Output

2.5 decades
4.2 decades
10.0 decades
'Hello' → long? false
'Groovy is a great language' → long? false
'The quick brown fox jumps over the lazy dog' → long? true
Round-trip 42.5: 42.5
Same value: true

What happened here: Composition doesn’t care about types – it just passes the output of one closure as the input to the next. This lets you build pipelines that transform data across completely different types: String to Integer to Double to String, or String to List to Integer to Boolean. The only rule is that the output type of one closure must be compatible with the input type of the next. Groovy’s dynamic typing makes this natural and flexible.

Example 4: Composing with Method Pointers

What we’re doing: Combining method pointers (from our previous post) with closure composition.

Example 4: Method Pointer Composition

// Compose method pointers directly
def normalize = String.&trim >> String.&toLowerCase
println "Normalized: '${normalize('  HELLO WORLD  ')}'"

// Mix method pointers with regular closures
def parseInt = Integer.&parseInt
def isEven = { it % 2 == 0 }
def toYesNo = { it ? "Yes" : "No" }

def isStringEven = parseInt >> isEven >> toYesNo

['1', '2', '3', '4', '5'].each { n ->
    println "${n} is even? ${isStringEven(n)}"
}

// Compose with static method pointers
def abs = Math.&abs
def sqrt = Math.&sqrt
def format = { String.format("%.4f", it) }

def safeSqrt = abs >> sqrt >> format
println "sqrt(|-16|) = ${safeSqrt(-16)}"
println "sqrt(|25|)  = ${safeSqrt(25)}"

// Compose GDK method pointers
def reverse = String.&reverse
def shoutReversed = String.&trim >> String.&toUpperCase >> reverse
println "Reversed: ${shoutReversed('  hello groovy  ')}"

Output

Normalized: 'hello world'
1 is even? No
2 is even? Yes
3 is even? No
4 is even? Yes
5 is even? No
sqrt(|-16|) = 4.0000
sqrt(|25|)  = 5.0000
Reversed: YVOORG OLLEH

What happened here: Method pointers are closures, so they compose just like any other closure. The String.&trim >> String.&toLowerCase composition is especially clean – it reads as “trim, then lowercase.” You can freely mix method pointers with regular closures in the same pipeline. This combination of method pointers and composition is where Groovy’s functional programming really starts to feel natural.

Example 5: Dynamic Pipeline Construction

What we’re doing: Building composition pipelines dynamically from a list of closures at runtime.

Example 5: Dynamic Pipeline Construction

// Define reusable transformation steps
def transformations = [
    trim:     { it.toString().trim() },
    lower:    { it.toLowerCase() },
    upper:    { it.toUpperCase() },
    reverse:  { it.reverse() },
    exclaim:  { it + "!" },
    question: { it + "?" },
    brackets: { "[${it}]" }
]

// Build a pipeline from a list of step names
def buildPipeline(Map<String, Closure> steps, List<String> names) {
    def pipeline = steps[names[0]]
    names.drop(1).each { name ->
        pipeline = pipeline >> steps[name]
    }
    pipeline
}

// Pipeline 1: trim → upper → exclaim
def shout = buildPipeline(transformations, ['trim', 'upper', 'exclaim'])
println "Shout: ${shout('  hello  ')}"

// Pipeline 2: trim → lower → reverse → brackets
def fancy = buildPipeline(transformations, ['trim', 'lower', 'reverse', 'brackets'])
println "Fancy: ${fancy('  GROOVY  ')}"

// Pipeline 3: trim → question
def ask = buildPipeline(transformations, ['trim', 'question'])
println "Ask: ${ask('  Is this working  ')}"

// Using inject for more concise pipeline building
def buildPipeline2(Map<String, Closure> steps, List<String> names) {
    names.collect { steps[it] }.inject { acc, fn -> acc >> fn }
}

def pipeline = buildPipeline2(transformations, ['trim', 'upper', 'brackets', 'exclaim'])
println "Inject pipeline: ${pipeline('  groovy  ')}"

Output

Shout: HELLO!
Fancy: [yvoorg]
Ask: Is this working?
Inject pipeline: [GROOVY]!

What happened here: We created a registry of named transformations, then built pipelines by specifying step names. The buildPipeline function composes closures dynamically using >>. The inject version is even more concise – it folds the list of closures into a single composed closure. This pattern is incredibly powerful for configurable processing: instead of hardcoding a pipeline, you let users specify which steps to run and in what order.

Example 6: Composition with Error Handling

What we’re doing: Adding error handling to composed closures so the pipeline doesn’t break on bad input.

Example 6: Error-Safe Composition

// Wrapper that catches exceptions in a closure
def safeClosure(String stepName, Closure fn) {
    return { input ->
        try {
            fn(input)
        } catch (Exception e) {
            println "  ERROR in '${stepName}': ${e.message} (input: ${input})"
            null
        }
    }
}

// Wrapper that skips null values
def skipNull(Closure fn) {
    return { input ->
        input == null ? null : fn(input)
    }
}

def parse  = safeClosure('parse', { Integer.parseInt(it.toString().trim()) })
def double_ = skipNull({ it * 2 })
def format = skipNull({ "Result: ${it}" })

def pipeline = parse >> double_ >> format

// Good inputs
println pipeline("21")
println pipeline("  50  ")

// Bad input - handled gracefully
println pipeline("not_a_number")
println pipeline("")

// Building a fully safe pipeline
def safeCompose(List<Closure> steps) {
    steps.inject { acc, fn -> acc >> skipNull(fn) }
}

def safePipeline = safeCompose([
    safeClosure('trim', { it.trim() }),
    safeClosure('parse', { Integer.parseInt(it) }),
    safeClosure('multiply', { it * 3 }),
    safeClosure('format', { "= ${it}" })
])

println safePipeline("  15  ")
println safePipeline("  bad  ")

Output

Result: 42
Result: 100
  ERROR in 'parse': For input string: "not_a_number" (input: not_a_number)
null
  ERROR in 'parse': For input string: "" (input: )
null
= 45
  ERROR in 'parse': For input string: "bad" (input: bad)
null

What happened here: Raw composition will blow up if any step throws an exception. The safeClosure wrapper catches exceptions and returns null instead, while skipNull short-circuits the rest of the pipeline when it encounters null. This is a simplified version of the “Railway-Oriented Programming” pattern – the pipeline has a happy path and an error path, and errors propagate cleanly without crashing.

Example 7: Composing Predicates (Boolean Closures)

What we’re doing: Combining boolean-returning closures into compound filters.

Example 7: Predicate Composition

// Individual predicates
def isPositive = { it > 0 }
def isEven = { it % 2 == 0 }
def isSmall = { it < 100 }

// Combine predicates with AND logic
def and(Closure... predicates) {
    return { value -> predicates.every { p -> p(value) } }
}

// Combine predicates with OR logic
def or(Closure... predicates) {
    return { value -> predicates.any { p -> p(value) } }
}

// Negate a predicate
def not(Closure predicate) {
    return { value -> !predicate(value) }
}

def numbers = [-20, -5, 0, 3, 8, 15, 42, 100, 256]

// Positive AND even AND small
def positiveEvenSmall = and(isPositive, isEven, isSmall)
println "Positive, even, small: ${numbers.findAll(positiveEvenSmall)}"

// Positive OR even
def positiveOrEven = or(isPositive, isEven)
println "Positive or even: ${numbers.findAll(positiveOrEven)}"

// NOT positive (i.e., zero or negative)
println "Not positive: ${numbers.findAll(not(isPositive))}"

// Complex: (positive AND even) OR (negative AND odd)
def isNegative = not(isPositive)
def isOdd = not(isEven)
def complex = or(and(isPositive, isEven), and(isNegative, isOdd))
println "Complex filter: ${numbers.findAll(complex)}"

// Use with composition: transform, then filter
def absValue = { Math.abs(it) }
def isAbsEven = absValue >> isEven
println "Abs-even: ${numbers.findAll(isAbsEven)}"

Output

Positive, even, small: [8, 42]
Positive or even: [-20, 0, 3, 8, 15, 42, 100, 256]
Not positive: [-20, -5, 0]
Complex filter: [-5, 8, 42, 100, 256]
Abs-even: [-20, 0, 8, 42, 100, 256]

What happened here: While >> composes sequentially (output feeds into input), predicate composition combines results logically. We built and(), or(), and not() combinator functions that take predicate closures and return new predicate closures. The last example mixes both approaches – absValue >> isEven first transforms the value, then tests it. This gives you a complete toolkit for building complex filters from simple, testable parts.

Example 8: Closure Composition for Data Validation

What we’re doing: Building a composable validation framework using closure composition.

Example 8: Composable Validation

// A validation result
class ValidationResult {
    boolean valid
    String value
    List<String> errors

    ValidationResult(boolean valid, String value, List<String> errors = []) {
        this.valid = valid
        this.value = value
        this.errors = errors
    }

    String toString() {
        valid ? "VALID: '${value}'" : "INVALID: ${errors}"
    }
}

// Validation step builder
def validate(String errorMsg, Closure<Boolean> predicate) {
    return { ValidationResult result ->
        if (!result.valid) return result  // Short-circuit on prior failure
        if (predicate(result.value)) {
            result
        } else {
            new ValidationResult(false, result.value, result.errors + errorMsg)
        }
    }
}

// Transformation step
def transform(Closure<String> fn) {
    return { ValidationResult result ->
        if (!result.valid) return result
        new ValidationResult(true, fn(result.value))
    }
}

// Compose validation pipeline
def validateEmail =
    transform({ it.trim().toLowerCase() }) >>
    validate("Email cannot be empty", { it.length() > 0 }) >>
    validate("Must contain @", { it.contains('@') }) >>
    validate("Must have domain", { it.split('@').length == 2 && it.split('@')[1].contains('.') }) >>
    validate("No spaces allowed", { !it.contains(' ') })

// Test various emails
def emails = ['User@Example.COM', '  alice@gmail.com  ', 'invalid', '@', 'a@b.c', '']
emails.each { email ->
    def result = validateEmail(new ValidationResult(true, email))
    println "'${email.trim().padRight(20)}' → ${result}"
}

Output

'User@Example.COM    ' → VALID: 'user@example.com'
'alice@gmail.com     ' → VALID: 'alice@gmail.com'
'invalid             ' → INVALID: [Must contain @]
'@                   ' → INVALID: [Must have domain]
'a@b.c               ' → VALID: 'a@b.c'
'                    ' → INVALID: [Email cannot be empty]

What happened here: Each validation step is a closure that takes a ValidationResult and returns a ValidationResult. Failed validations short-circuit – once one step fails, subsequent steps pass through the failure without checking. Transformation steps (like trim and lowercase) modify the value inside the result. The >> operator chains everything together into a single validation pipeline. This pattern is extensible: add a new rule by composing another validate() step.

Example 9: Composing Collection Transformations

What we’re doing: Using composition to build reusable collection processing pipelines.

Example 9: Collection Pipeline Composition

// Collection-level transformations
def filterPositive = { list -> list.findAll { it > 0 } }
def sortAsc = { list -> list.sort(false) }
def doubleAll = { list -> list.collect { it * 2 } }
def takeTop3 = { list -> list.take(3) }
def sumAll = { list -> list.sum() }

// Compose into different pipelines
def top3Doubled = filterPositive >> sortAsc >> doubleAll >> takeTop3
def sumPositives = filterPositive >> sumAll

def data = [15, -3, 8, -7, 22, 1, -10, 45, 3]

println "Data: ${data}"
println "Top 3 doubled: ${top3Doubled(data)}"
println "Sum positives: ${sumPositives(data)}"

// String collection pipeline
def splitLines = { String text -> text.split('\n') as List }
def trimLines = { list -> list.collect { it.trim() } }
def removeEmpty = { list -> list.findAll { it.length() > 0 } }
def sortLines = { list -> list.sort(false) }
def joinWithComma = { list -> list.join(', ') }

def cleanAndSort = splitLines >> trimLines >> removeEmpty >> sortLines >> joinWithComma

def messyText = """
    banana
    apple

    cherry
    date

    elderberry
"""

println "Cleaned: ${cleanAndSort(messyText)}"

// Reuse parts in different pipelines
def cleanLines = splitLines >> trimLines >> removeEmpty
def countLines = cleanLines >> { it.size() }
def firstLine = cleanLines >> sortLines >> { it.first() }

println "Line count: ${countLines(messyText)}"
println "First (sorted): ${firstLine(messyText)}"

Output

Data: [15, -3, 8, -7, 22, 1, -10, 45, 3]
Top 3 doubled: [2, 6, 16]
Sum positives: 94
Cleaned: apple, banana, cherry, date, elderberry
Line count: 5
First (sorted): apple

What happened here: Each closure operates on a collection and returns a collection (or a scalar for terminal operations like sum). By composing them with >>, we build reusable processing pipelines. The key insight is that you can share pipeline segments – cleanLines is composed once and reused in both countLines and firstLine. This is composition at its best: small, focused closures assembled into bigger processing units.

Example 10: Decorator Pattern with Composition

What we’re doing: Using closure composition to implement the decorator pattern – adding behavior around an existing function.

Example 10: Decorator Pattern

// Logging decorator
def withLogging(String name, Closure fn) {
    return { Object... args ->
        println "  → Calling '${name}' with args: ${args as List}"
        def result = fn(*args)
        println "  ← '${name}' returned: ${result}"
        result
    }
}

// Timing decorator
def withTiming(String name, Closure fn) {
    return { Object... args ->
        def start = System.nanoTime()
        def result = fn(*args)
        def elapsed = (System.nanoTime() - start) / 1_000_000.0
        println "  ⏱ '${name}' took ${String.format('%.3f', elapsed)} ms"
        result
    }
}

// Caching decorator
def withCache(Closure fn) {
    def cache = [:]
    return { Object... args ->
        def key = args as List
        if (!cache.containsKey(key)) {
            cache[key] = fn(*args)
        }
        cache[key]
    }
}

// Base function
def expensiveCalculation = { int n ->
    Thread.sleep(100)  // Simulate expensive work
    n * n + n
}

// Decorate it
def logged = withLogging('calculate', expensiveCalculation)
def timed = withTiming('calculate', expensiveCalculation)
def cached = withCache(expensiveCalculation)
def fullDecorated = withLogging('cached-calc', withCache(expensiveCalculation))

println "=== Logged ==="
logged(5)

println "\n=== Timed ==="
timed(5)

println "\n=== Cached (first call) ==="
fullDecorated(10)

println "\n=== Cached (second call - instant) ==="
fullDecorated(10)

Output

=== Logged ===
  → Calling 'calculate' with args: [5]
  ← 'calculate' returned: 30

=== Timed ===
  ⏱ 'calculate' took 105.234 ms

=== Cached (first call) ===
  → Calling 'cached-calc' with args: [10]
  ← 'cached-calc' returned: 110

=== Cached (second call - instant) ===
  → Calling 'cached-calc' with args: [10]
  ← 'cached-calc' returned: 110

What happened here: Decorators are closures that wrap other closures, adding behavior before and after the original call. withLogging adds input/output logging, withTiming measures execution time, and withCache memoizes results. You can stack decorators by nesting – withLogging('cached-calc', withCache(expensiveCalculation)) creates a function that logs and caches. The second cached call is instant because the result is served from the cache.

Example 11: Composition with Closures Returning Closures

What we’re doing: Creating higher-order closure factories and composing their outputs.

Example 11: Closure Factories and Composition

// Factory: creates a multiplier closure
def multiplier = { factor -> return { it * factor } }

// Factory: creates an adder closure
def adder = { amount -> return { it + amount } }

// Factory: creates a formatter closure
def formatter = { String template -> return { String.format(template, it) } }

// Build a pipeline from factory outputs
def pipeline = multiplier(3) >> adder(10) >> formatter("Result: %d")

println pipeline(5)   // 5*3=15, 15+10=25, format
println pipeline(10)  // 10*3=30, 30+10=40, format

// Create different pipelines from the same factories
def tax = multiplier(1.08) >> formatter("Price with tax: \$%.2f")
def discount = multiplier(0.80) >> formatter("After 20%% off: \$%.2f")

println tax(100)
println discount(100)

// Compose factories themselves
def makeProcessor = { mult, add, String fmt ->
    multiplier(mult) >> adder(add) >> formatter(fmt)
}

def celsiusToFahrenheit = makeProcessor(9.0/5.0, 32, "%.1f°F")
// More precise version with doubles
def c2f = { it * 9.0 / 5.0 } >> { it + 32 } >> { String.format("%.1f°F", it) }

[0, 20, 37, 100].each { c ->
    println "${c}°C = ${c2f(c)}"
}

Output

Result: 25
Result: 40
Price with tax: $108.00
After 20% off: $80.00
0°C = 32.0°F
20°C = 68.0°F
37°C = 98.6°F
100°C = 212.0°F

What happened here: Closure factories are functions that produce other functions. When you compose their outputs, you get reusable, parameterized pipelines. The multiplier(3) >> adder(10) expression first creates two closures, then composes them into a single closure. This is the functional programming equivalent of the builder pattern – you configure behavior by combining simple, focused building blocks. The temperature conversion example shows how inline closures can compose just as cleanly as named ones.

Example 12: Real-World Data Processing Pipeline

What we’re doing: Building a complete data processing pipeline for transforming raw CSV-like data into a formatted report.

Example 12: Real-World Data Pipeline

// Raw data (simulating CSV records)
def rawData = """
name,department,salary
Alice,Engineering,95000
Bob,Marketing,72000
Charlie,Engineering,88000
Diana,Marketing,81000
Eve,Engineering,102000
Frank,HR,65000
Grace,HR,70000
"""

// Step 1: Parse CSV into list of maps
def parseCSV = { String csv ->
    def lines = csv.trim().split('\n') as List
    def headers = lines[0].split(',')
    lines.drop(1).collect { line ->
        def values = line.split(',')
        [headers, values].transpose().collectEntries()
    }
}

// Step 2: Clean and transform records
def cleanRecords = { List<Map> records ->
    records.collect { record ->
        [
            name: record.name.trim(),
            department: record.department.trim(),
            salary: record.salary.trim().toInteger()
        ]
    }
}

// Step 3: Filter by minimum salary
def filterBySalary = { int minSalary ->
    return { List<Map> records ->
        records.findAll { it.salary >= minSalary }
    }
}

// Step 4: Group by department
def groupByDept = { List<Map> records ->
    records.groupBy { it.department }
}

// Step 5: Calculate department summaries
def summarize = { Map<String, List<Map>> grouped ->
    grouped.collectEntries { dept, employees ->
        [dept, [
            count: employees.size(),
            avgSalary: employees.salary.sum() / employees.size(),
            topEarner: employees.max { it.salary }.name
        ]]
    }
}

// Step 6: Format report
def formatReport = { Map<String, Map> summaries ->
    def lines = ["=== Department Report ==="]
    summaries.sort { it.key }.each { dept, stats ->
        lines << "${dept}:"
        lines << "  Employees:  ${stats.count}"
        lines << "  Avg Salary: \$${String.format('%,.0f', stats.avgSalary)}"
        lines << "  Top Earner: ${stats.topEarner}"
    }
    lines.join('\n')
}

// Compose the full pipeline
def generateReport = parseCSV >> cleanRecords >> filterBySalary(70000) >> groupByDept >> summarize >> formatReport

println generateReport(rawData)

println "\n--- Without salary filter ---"
def fullReport = parseCSV >> cleanRecords >> groupByDept >> summarize >> formatReport
println fullReport(rawData)

Output

=== Department Report ===
Engineering:
  Employees:  3
  Avg Salary: $95,000
  Top Earner: Eve
HR:
  Employees:  1
  Avg Salary: $70,000
  Top Earner: Grace
Marketing:
  Employees:  2
  Avg Salary: $76,500
  Top Earner: Diana

--- Without salary filter ---
=== Department Report ===
Engineering:
  Employees:  3
  Avg Salary: $95,000
  Top Earner: Eve
HR:
  Employees:  2
  Avg Salary: $67,500
  Top Earner: Grace
Marketing:
  Employees:  2
  Avg Salary: $76,500
  Top Earner: Diana

What happened here: This is a complete data processing pipeline – from raw CSV text to a formatted report – built entirely from composed closures. Each step is a standalone, testable function. The filterBySalary(70000) call is a closure factory that produces a filter closure with the threshold baked in. Notice how easy it is to create a variation: fullReport skips the salary filter by simply omitting that step from the composition. This is the ultimate payoff of closure composition – complex processing expressed as a simple, readable chain of transformations.

Composition Patterns and Idioms

Pattern 1: Identity Composition

Identity Composition

// Identity closure - returns input unchanged
def identity = { it }

// Useful as a default or starting point for dynamic composition
def steps = [{ it * 2 }, { it + 1 }]
def pipeline = steps.inject(identity) { acc, fn -> acc >> fn }
println "With steps: ${pipeline(5)}"     // 5*2=10, 10+1=11

// Empty pipeline
def emptyPipeline = [].inject(identity) { acc, fn -> acc >> fn }
println "No steps: ${emptyPipeline(5)}"  // 5 - just passes through

Output

With steps: 11
No steps: 5

Pattern 2: Conditional Composition

Conditional Composition

// Add a step only if a condition is met
def conditionalStep(boolean condition, Closure step) {
    condition ? step : { it }  // Return identity if condition is false
}

def debugMode = true
def pipeline = { it.trim() } >>
    conditionalStep(debugMode, { println "  DEBUG: '${it}'"; it }) >>
    { it.toUpperCase() }

println pipeline("  hello  ")

Output

DEBUG: 'hello'
HELLO

Pattern 3: Fan-Out Composition

Fan-Out Pattern

// Apply multiple closures to the same input, collect results
def fanOut(Closure... fns) {
    return { input -> fns.collect { fn -> fn(input) } }
}

def analyze = fanOut(
    { it.length() },
    { it.toUpperCase() },
    { it.reverse() },
    { it.count('o') }
)

def results = analyze("groovy")
println "Length: ${results[0]}, Upper: ${results[1]}, Reversed: ${results[2]}, O-count: ${results[3]}"

Output

Length: 6, Upper: GROOVY, Reversed: yvoorg, O-count: 2

These patterns extend composition beyond simple linear pipelines. The identity pattern gives you a safe starting point for dynamic composition. Conditional composition lets you include or skip steps based on runtime conditions. Fan-out applies multiple transformations in parallel and collects all results.

Best Practices

DO:

  • Use >> as your default composition operator – left-to-right reads naturally
  • Keep individual closures small and focused – each should do one thing well
  • Name your intermediate closures – def clean = trim >> lower is more readable than inlining everything
  • Use the identity closure { it } as a starting point when building pipelines dynamically with inject
  • Add error handling wrappers when composing closures that might fail on bad input

DON’T:

  • Create extremely long composition chains – break them into named segments for readability
  • Forget that composition creates a new closure each time – avoid composing in tight loops
  • Mix << and >> in the same expression – it makes the data flow direction confusing
  • Use composition when a simple method chain would be clearer – str.trim().toLowerCase() is fine for one-off operations
  • Compose closures with side effects unless you carefully control execution order

Conclusion

We’ve covered everything about Groovy closure composition – from the basic >> and << operators to dynamic pipeline construction, error handling, predicate composition, decorator patterns, and real-world data processing. Closure composition is the technique that transforms closures from isolated code blocks into composable building blocks for clean, functional-style Groovy code.

The big takeaway: think of closures as LEGO bricks. Each one does one thing. The >> operator snaps them together. The resulting pipeline is itself a closure, so you can compose it further or pass it wherever a closure is expected. Combined with closures and higher-order functions, composition gives you a complete functional programming toolkit right inside Groovy.

Summary

  • >> composes left-to-right (andThen), << composes right-to-left (compose) – prefer >> for readability
  • Composed closures are closures themselves – you can compose them further, curry them, or pass them as arguments
  • Dynamic pipeline construction with inject lets you build configurable processing chains at runtime
  • Error handling wrappers and predicate combinators extend composition beyond simple sequential pipelines
  • Keep individual closures focused on a single transformation – composition handles the orchestration

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 Functional Interfaces and SAM Types

Frequently Asked Questions

What is the difference between >> and << for closure composition in Groovy?

The >> operator (rightShift/andThen) applies the left closure first, then passes its result to the right closure. The << operator (leftShift/compose) applies the right closure first, then the left. Think of the arrows as showing data flow direction. Most developers prefer >> because left-to-right order matches natural reading order.

Can I compose more than two closures in Groovy?

Yes. You can chain as many closures as you want: def pipeline = step1 >> step2 >> step3 >> step4. Each >> creates a new composed closure, and the result is itself a closure, so it can be composed further. For dynamic composition from a list of closures, use inject: closures.inject { acc, fn -> acc >> fn }.

What happens if a composed closure throws an exception?

The exception propagates normally – the pipeline stops at the failing step and the exception bubbles up to the caller. If you want graceful error handling, wrap individual steps in error-handling closures that catch exceptions and return a default value or an error object. This is similar to the Railway-Oriented Programming pattern.

Can I compose closures with different parameter types?

Yes. Groovy’s dynamic typing allows composing closures where the output type of one differs from the input type of the next. For example, you can compose a closure that returns an Integer with one that accepts an Integer and returns a String. The only requirement is that the runtime types are compatible at execution time.

How does Groovy closure composition compare to Java Function.andThen()?

Groovy’s >> operator is similar to Java’s Function.andThen(), and << is similar to Function.compose(). The key difference is that Groovy’s composition works with Closure objects (which are more powerful than Java’s Function interface) and supports dynamic typing, varargs, and currying. Java’s version is limited to single-argument functions unless you create custom functional interfaces.

Interview Questions on Groovy Closure Composition

“These come from real screens. Practice answering before you read each answer.”

Q: A data-cleanup pipeline is written as validate << sanitize, and records keep failing validation on raw input. The author swears the order is right because validate “comes first”. What is wrong?

The author is reading << left to right, but it composes right to left: validate << sanitize means validate-of-sanitize, so sanitize runs first and validation sees clean input. If records are failing on raw input, somewhere the operands are the other way around, sanitize << validate, which validates first. The way to keep it straight aloud: a << b is a(b(x)), a >> b is b(a(x)), and a >> b equals b << a. In review I push for >> everywhere, because execution order then matches reading order and nobody has to decode math notation under deadline.

Q: A five-stage composed pipeline throws a NullPointerException, and the stack trace is a wall of doCall frames that does not say which stage failed. How do you debug and prevent this?

Composed closures all look like anonymous doCall frames, so the trace tells you the pipeline failed, not where. Quick debugging: splice in a passthrough stage like { x -> println x; x } between stages, or run the stages one at a time in groovysh with the failing input. Prevention is structural: name each stage as a variable so intent is testable, unit-test stages individually in Spock, and make stages total, meaning they handle null and edge inputs instead of assuming the previous stage was perfect. Usually the real bug is one stage returning null that the next never expected.

Q: Your first pipeline stage takes two arguments, { rate, order -> ... }, and composing it with later one-argument stages works fine. Then someone moves the two-argument closure to the middle and it breaks. Why?

Composition passes exactly one value between stages: whatever the previous closure returned becomes the single argument of the next. Only the first stage gets the caller’s full argument list, so a multi-parameter closure is legal at the head of the chain and nowhere else. When the two-argument stage lands mid-chain it receives one value and dies with a MissingMethodException at call time, not composition time, which is the sneaky part. Fixes: curry the extra parameter away before composing, or pass a map or small object through the chain so every stage has a one-argument shape.

Q: A rules engine composes user-configured rules with rules.inject { acc, r -> acc >> r }. It crashes the day a customer has zero rules. What is the clean fix?

Seedless inject needs at least one element, so an empty rule list throws NoSuchElementException. Function composition has a natural identity element, and Groovy ships it: Closure.IDENTITY, the closure that returns its argument unchanged. Seed the fold with it, rules.inject(Closure.IDENTITY) { acc, r -> acc >> r }, and zero rules now yields a pipeline that passes data through untouched, which is the correct business behavior. Bringing up identity elements unprompted signals you actually understand composition as an operation with algebraic structure, not just operator syntax.

Q: A teammate wants retry-with-logging around every stage of a composed pipeline and starts editing each closure by hand. What is the composition-friendly alternative?

Write a decorator: a higher-order closure that takes a stage and returns a wrapped stage, something like def resilient = { stage -> { x -> retryWithLog { stage(x) } } }. Then build the pipeline from decorated stages, steps.collect(resilient).inject { a, b -> a >> b }, and cross-cutting behavior lives in exactly one place. This is the same idea as Java’s Function wrappers or Spring AOP, expressed in five lines of Groovy. The point to land: because composed stages are just values, you transform the stages themselves rather than editing their bodies.

Previous in Series: Groovy Method References and Method Pointers

Next in Series: Groovy Functional Interfaces and SAM Types

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 *