Groovy Trampoline – Recursive Closures Without Stack Overflow (10+ Examples)

Groovy trampoline() enables stack-safe recursive closures. 10+ tested examples covering factorial, Fibonacci, tree traversal, and tail-call optimization.

“Recursion is elegant until it blows the stack. Trampolining keeps the elegance and throws away the stack frames.”

Guy Steele, Lambda Papers

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 18 minutes

Recursion makes code elegant and expressive, but it has a dirty secret on the JVM: every recursive call adds a frame to the call stack, and the stack has a finite size. Call a function deep enough and you get the dreaded StackOverflowError. The groovy trampoline method solves this by converting recursive closures into iterative loops that reuse a single stack frame.

Some languages solve this with tail-call optimization (TCO) – the compiler detects that the last thing a function does is call itself, and reuses the current stack frame instead of creating a new one. But the JVM doesn’t support TCO natively. Neither does Java. And by extension, neither does Groovy – at least not automatically.

Enter .trampoline(). This Groovy closure method gives you stack-safe recursion by converting recursive calls into an iterative loop internally. You write recursive code, but it executes iteratively – no stack overflow, no matter how deep the recursion goes.

In this post, we’ll explore Groovy trampoline with 10+ tested examples, from simple factorial to mutual recursion, tree traversal, and state machines. If you’re new to closures, start with our Groovy Closures Complete Guide. If you want to combine trampolining with caching, check out our Groovy Memoization post.

Let’s make recursion safe.

The Stack Overflow Problem

Before we look at the solution, let’s understand the problem clearly.

Every time a function calls itself recursively, the JVM pushes a new frame onto the call stack. This frame holds local variables, the return address, and other bookkeeping data. The default JVM stack size is typically around 512KB to 1MB, which allows roughly 5,000 to 15,000 recursive calls before you get a StackOverflowError.

For many problems, that’s not enough. Computing the 100,000th number in a sequence, traversing a deeply nested tree, or processing a long linked list recursively will all blow the stack. You could increase the stack size with -Xss, but that’s a band-aid, not a solution.

According to the official Groovy closures documentation, the trampoline() method solves this by converting tail-recursive closures into iterative execution. The recursive call doesn’t actually recurse – it returns a special TrampolineClosure object that the trampoline loop evaluates iteratively.

How Trampoline Works

The trampoline pattern works in three steps:

  1. You call .trampoline() on a closure – this wraps it in a TrampolineClosure
  2. Inside the closure, recursive calls return a thunk – instead of actually recursing, the closure returns a deferred computation
  3. The trampoline loop evaluates thunks iteratively – it keeps calling the returned thunks in a while loop until a non-thunk value is returned (the final result)

Think of it like a real trampoline: instead of stacking function calls on top of each other (which eventually topples), each call “bounces” back to the ground level before the next call. The stack never grows beyond a single frame.

Key requirement: Your closure must be tail-recursive – the recursive call must be the very last operation in the closure. If you do something with the result after the recursive call (like n * factorial(n-1)), it’s not tail-recursive and won’t work correctly with trampoline.

10 Practical Examples

Example 1: The Stack Overflow Problem Demonstrated

What we’re doing: First showing how regular recursion fails, then fixing it with trampoline.

Example 1: Stack Overflow vs Trampoline

// Regular recursive closure - will blow the stack
def countdownRegular
countdownRegular = { int n ->
    if (n == 0) return "Done!"
    countdownRegular(n - 1)  // Direct recursive call
}

// Works for small values
println "Regular countdown(100): ${countdownRegular(100)}"

// Fails for large values
try {
    countdownRegular(100000)
} catch (StackOverflowError e) {
    println "Regular countdown(100000): StackOverflowError!"
}

// Trampoline version - stack-safe!
def countdownTrampoline
countdownTrampoline = { int n ->
    if (n == 0) return "Done!"
    countdownTrampoline.trampoline(n - 1)  // Returns a thunk, doesn't recurse
}.trampoline()

// Works for ANY value - no stack overflow
println "Trampoline countdown(100): ${countdownTrampoline(100)}"
println "Trampoline countdown(100000): ${countdownTrampoline(100000)}"
println "Trampoline countdown(1000000): ${countdownTrampoline(1000000)}"

Output

Regular countdown(100): Done!
Regular countdown(100000): StackOverflowError!
Trampoline countdown(100): Done!
Trampoline countdown(100000): Done!
Trampoline countdown(1000000): Done!

What happened here: The regular recursive closure blows the stack at around 100,000 calls. The trampoline version handles 1,000,000 calls without breaking a sweat. Notice the two changes: (1) we call .trampoline() on the closure definition, and (2) inside the closure, we call countdownTrampoline.trampoline(n - 1) instead of countdownTrampoline(n - 1). The .trampoline() call inside returns a thunk (a deferred computation) instead of actually recursing. The outer trampoline loop then evaluates these thunks iteratively.

Example 2: Tail-Recursive Factorial

What we’re doing: Converting the classic factorial function to tail-recursive form and using trampoline to make it stack-safe.

Example 2: Factorial with Trampoline

// WRONG: This is NOT tail-recursive
// The multiplication happens AFTER the recursive call returns
def factorialBad = { int n ->
    if (n <= 1) return 1G  // BigInteger
    n * call(n - 1)  // NOT tail-recursive: n * (recursive result)
}

// RIGHT: Tail-recursive with accumulator
def factorialTail
factorialTail = { BigInteger n, BigInteger acc = 1G ->
    if (n <= 1) return acc
    factorialTail.trampoline(n - 1, n * acc)  // Tail position!
}.trampoline()

// Test with small values
println "factorial(5)  = ${factorialTail(5)}"
println "factorial(10) = ${factorialTail(10)}"
println "factorial(20) = ${factorialTail(20)}"

// Test with large values - no stack overflow!
println "factorial(100) = ${factorialTail(100)}"
println "factorial(1000) has ${factorialTail(1000).toString().size()} digits"
println "factorial(10000) has ${factorialTail(10000).toString().size()} digits"
println "factorial(50000) has ${factorialTail(50000).toString().size()} digits"

Output

factorial(5)  = 120
factorial(10) = 3628800
factorial(20) = 2432902008176640000
factorial(100) = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
factorial(1000) has 2568 digits
factorial(10000) has 35660 digits
factorial(50000) has 213237 digits

What happened here: The key transformation is adding an accumulator parameter. Instead of n * factorial(n-1) (which needs the result before multiplying), we pass the accumulated result forward: factorialTail(n-1, n * acc). The recursive call is now the absolute last thing that happens – no operation depends on its return value. This is what makes it tail-recursive and compatible with trampoline. We computed factorial of 50,000 (a number with over 200,000 digits) without any stack issues.

Example 3: Sum of a Range with Trampoline

What we’re doing: Summing a large range of numbers recursively – a simple example to solidify the accumulator pattern.

Example 3: Recursive Sum

// Tail-recursive sum with trampoline
def sumRange
sumRange = { long n, long acc = 0 ->
    if (n == 0) return acc
    sumRange.trampoline(n - 1, acc + n)
}.trampoline()

// Small values
println "sum(10)     = ${sumRange(10)}"
println "sum(100)    = ${sumRange(100)}"
println "sum(1000)   = ${sumRange(1000)}"

// Large values - would crash without trampoline
println "sum(100000) = ${sumRange(100000)}"
println "sum(500000) = ${sumRange(500000)}"

// Verify against formula: n*(n+1)/2
def n = 100000L
assert sumRange(n) == n * (n + 1) / 2
println "\nVerified: sumRange(${n}) == ${n}*(${n}+1)/2 = ${n * (n + 1) / 2}"

// Tail-recursive product (similar pattern)
def productRange
productRange = { long from, long to, BigInteger acc = 1G ->
    if (from > to) return acc
    productRange.trampoline(from + 1, to, acc * from)
}.trampoline()

println "\nproduct(1..10)  = ${productRange(1, 10)}"
println "product(1..20)  = ${productRange(1, 20)}"

Output

sum(10)     = 55
sum(100)    = 5050
sum(1000)   = 500500
sum(100000) = 5000050000
sum(500000) = 125000250000

Verified: sumRange(100000) == 100000*(100000+1)/2 = 5000050000

product(1..10)  = 3628800
product(1..20)  = 2432902008176640000

What happened here: The pattern is always the same – pass the accumulated result as a parameter and make the recursive call the last operation. For sum, we pass acc + n forward. For product, we pass acc * from forward. The trampoline converts what would be 500,000 stack frames into a simple while loop. We verified the sum against the mathematical formula to confirm correctness.

Example 4: Fibonacci with Trampoline

What we’re doing: Computing Fibonacci numbers with trampoline – requires a clever two-accumulator approach since Fibonacci needs two previous values.

Example 4: Fibonacci with Trampoline

// Tail-recursive Fibonacci with two accumulators
def fibTrampoline
fibTrampoline = { long n, BigInteger a = 0G, BigInteger b = 1G ->
    if (n == 0) return a
    if (n == 1) return b
    fibTrampoline.trampoline(n - 1, b, a + b)
}.trampoline()

// Standard Fibonacci values
println "=== Fibonacci Sequence ==="
(0..15).each { print "${fibTrampoline(it)} " }
println ""

// Large Fibonacci numbers - no stack overflow
println "\n=== Large Fibonacci ==="
println "fib(100)   = ${fibTrampoline(100)}"
println "fib(1000)  = ${fibTrampoline(1000).toString().take(30)}... (${fibTrampoline(1000).toString().size()} digits)"
println "fib(10000) has ${fibTrampoline(10000).toString().size()} digits"
println "fib(50000) has ${fibTrampoline(50000).toString().size()} digits"

// Verify known values
assert fibTrampoline(10) == 55
assert fibTrampoline(20) == 6765
assert fibTrampoline(50) == 12586269025G
println "\nAll known values verified!"

Output

=== Fibonacci Sequence ===
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610

=== Large Fibonacci ===
fib(100)   = 354224848179261915075
fib(1000)  = 434665576869374564356885... (209 digits)
fib(10000) has 2090 digits
fib(50000) has 10450 digits

All known values verified!

What happened here: The trick with Fibonacci is using two accumulators – a (current value) and b (next value). Each step shifts the window: a becomes b, and b becomes a + b. This avoids the classic double-recursion problem entirely. We computed fib(50000) – a number with over 10,000 digits – without any stack issues. Compare this with the memoized approach from our memoization post – memoization caches intermediate results but still uses the stack, while trampoline avoids the stack entirely.

Example 5: String Processing with Trampoline

What we’re doing: Using trampoline for recursive string operations – reversing, counting, and pattern matching on very long strings.

Example 5: String Processing

// Tail-recursive string reversal
def reverseString
reverseString = { String str, String acc = '' ->
    if (str.isEmpty()) return acc
    reverseString.trampoline(str.substring(1), str[0] + acc)
}.trampoline()

println "reverse('Groovy') = ${reverseString('Groovy')}"
println "reverse('trampoline') = ${reverseString('trampoline')}"

// Tail-recursive character counting
def countChar
countChar = { String str, char target, int acc = 0 ->
    if (str.isEmpty()) return acc
    def newAcc = str[0] == target ? acc + 1 : acc
    countChar.trampoline(str.substring(1), target, newAcc)
}.trampoline()

def testStr = "mississippi"
println "\nCount 's' in '${testStr}': ${countChar(testStr, 's' as char)}"
println "Count 'i' in '${testStr}': ${countChar(testStr, 'i' as char)}"
println "Count 'p' in '${testStr}': ${countChar(testStr, 'p' as char)}"

// Tail-recursive palindrome check
def isPalindrome
isPalindrome = { String str ->
    if (str.size() <= 1) return true
    if (str[0] != str[-1]) return false
    isPalindrome.trampoline(str.substring(1, str.size() - 1))
}.trampoline()

['racecar', 'hello', 'madam', 'groovy', 'level', 'a'].each {
    println "isPalindrome('${it}'): ${isPalindrome(it)}"
}

// Test with a very long palindrome
def longPalindrome = 'a' * 50000 + 'b' + 'a' * 50000
println "\nLong palindrome (100,001 chars): ${isPalindrome(longPalindrome)}"
def notPalindrome = 'a' * 50000 + 'b' + 'a' * 49999 + 'c'
println "Long non-palindrome: ${isPalindrome(notPalindrome)}"

Output

reverse('Groovy') = yvoorG
reverse('trampoline') = enilopmart

Count 's' in 'mississippi': 4
Count 'i' in 'mississippi': 4
Count 'p' in 'mississippi': 2
isPalindrome('racecar'): true
isPalindrome('hello'): false
isPalindrome('madam'): true
isPalindrome('groovy'): false
isPalindrome('level'): true
isPalindrome('a'): true

Long palindrome (100,001 chars): true
Long non-palindrome: false

What happened here: All three string operations follow the tail-recursive pattern: process one character, pass the remainder and accumulated result forward. The palindrome check on a 100,001-character string would require 50,000 recursive calls – well beyond the stack limit for regular recursion, but trivial for trampoline. These examples show that Groovy trampoline isn’t just for math – it works for any recursive algorithm that can be written in tail position.

Example 6: Mutual Recursion with Trampoline

What we’re doing: Using trampoline with two closures that call each other – mutual recursion is one of trampoline’s most powerful use cases.

Example 6: Mutual Recursion

// isEven and isOdd defined in terms of each other
def isEven
def isOdd

isEven = { int n ->
    if (n == 0) return true
    isOdd.trampoline(n - 1)
}.trampoline()

isOdd = { int n ->
    if (n == 0) return false
    isEven.trampoline(n - 1)
}.trampoline()

// Small values
println "isEven(0) = ${isEven(0)}"
println "isEven(1) = ${isEven(1)}"
println "isEven(4) = ${isEven(4)}"
println "isOdd(7)  = ${isOdd(7)}"

// Large values - mutual recursion that would overflow the stack
println "\nisEven(100000) = ${isEven(100000)}"
println "isOdd(100001)  = ${isOdd(100001)}"
println "isEven(99999)  = ${isEven(99999)}"

// More practical mutual recursion: parsing nested expressions
// Simulates parsing alternating patterns: A -> B -> A -> B -> done
def parseA
def parseB

parseA = { List tokens, List result = [] ->
    if (tokens.isEmpty()) return result
    if (tokens[0] == 'A') {
        parseB.trampoline(tokens.tail(), result + ['Found A'])
    } else {
        result + ["Unexpected: ${tokens[0]}"]
    }
}.trampoline()

parseB = { List tokens, List result = [] ->
    if (tokens.isEmpty()) return result
    if (tokens[0] == 'B') {
        parseA.trampoline(tokens.tail(), result + ['Found B'])
    } else {
        result + ["Unexpected: ${tokens[0]}"]
    }
}.trampoline()

def tokens = ['A', 'B', 'A', 'B', 'A', 'B']
println "\nParsing ${tokens}: ${parseA(tokens)}"

Output

isEven(0) = true
isEven(1) = false
isEven(4) = true
isOdd(7)  = true

isEven(100000) = true
isOdd(100001)  = true
isEven(99999)  = false

Parsing [A, B, A, B, A, B]: [Found A, Found B, Found A, Found B, Found A, Found B]

What happened here: Mutual recursion is when two functions call each other – isEven calls isOdd, and isOdd calls isEven. Without trampoline, this would blow the stack for large numbers (100,000 mutual calls). With trampoline on both closures, the bouncing between them happens iteratively. The parser example shows a more practical use – processing alternating tokens where two parsing functions hand off to each other. This pattern appears in real parsers, state machines, and protocol handlers.

Example 7: List Processing with Trampoline

What we’re doing: Processing large lists recursively – filtering, mapping, and folding with trampoline.

Example 7: List Processing

// Tail-recursive list filter
def filterList
filterList = { List items, Closure predicate, List acc = [] ->
    if (items.isEmpty()) return acc
    def newAcc = predicate(items[0]) ? acc + items[0] : acc
    filterList.trampoline(items.tail(), predicate, newAcc)
}.trampoline()

def numbers = (1..100).toList()
def evens = filterList(numbers, { it % 2 == 0 })
println "Evens (first 10): ${evens.take(10)}"
println "Even count: ${evens.size()}"

// Tail-recursive list map
def mapList
mapList = { List items, Closure transform, List acc = [] ->
    if (items.isEmpty()) return acc
    mapList.trampoline(items.tail(), transform, acc + transform(items[0]))
}.trampoline()

def doubled = mapList((1..10).toList(), { it * 2 })
println "\nDoubled: ${doubled}"

// Tail-recursive reduce (fold left)
def foldLeft
foldLeft = { List items, initial, Closure operation ->
    if (items.isEmpty()) return initial
    foldLeft.trampoline(items.tail(), operation(initial, items[0]), operation)
}.trampoline()

def sum = foldLeft((1..100).toList(), 0, { acc, val -> acc + val })
println "Sum 1..100: ${sum}"

def product = foldLeft((1..10).toList(), 1G, { acc, val -> acc * val })
println "Product 1..10: ${product}"

// Large list processing - would stack overflow without trampoline
def largeList = (1..50000).toList()
def largeSum = foldLeft(largeList, 0L, { acc, val -> acc + val })
println "\nSum 1..50000: ${largeSum}"
println "Verified: ${largeSum == 50000L * 50001L / 2}"

Output

Evens (first 10): [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Even count: 50

Doubled: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Sum 1..100: 5050
Product 1..10: 3628800

Sum 1..50000: 1250025000
Verified: true

What happened here: We reimplemented findAll, collect, and inject using tail-recursive trampoline closures. In practice, you’d use Groovy’s built-in methods for these operations since they’re already iterative. But this example demonstrates the pattern: any list-processing recursion can be made stack-safe with an accumulator and trampoline. The fold over 50,000 elements works without issues, while the equivalent regular recursion would fail around 10,000-15,000 elements.

Example 8: GCD and Number Theory with Trampoline

What we’re doing: Computing Greatest Common Divisor and other number-theoretic functions that are naturally recursive.

Example 8: GCD with Trampoline

// Euclidean algorithm - naturally tail-recursive!
def gcd
gcd = { BigInteger a, BigInteger b ->
    if (b == 0) return a
    gcd.trampoline(b, a % b)
}.trampoline()

println "gcd(48, 18)    = ${gcd(48, 18)}"
println "gcd(100, 75)   = ${gcd(100, 75)}"
println "gcd(1071, 462) = ${gcd(1071, 462)}"

// LCM using GCD
def lcm = { BigInteger a, BigInteger b -> (a * b).abs().intdiv(gcd(a, b)) }
println "\nlcm(12, 18) = ${lcm(12, 18)}"
println "lcm(7, 13)  = ${lcm(7, 13)}"

// Very large numbers - GCD still works!
def huge1 = 123456789012345678901234567890G
def huge2 = 987654321098765432109876543210G
println "\ngcd(huge1, huge2) = ${gcd(huge1, huge2)}"

// Collatz conjecture: for any n, repeatedly apply:
//   if even: n/2
//   if odd:  3n+1
// Eventually reaches 1 (unproven but true for all tested numbers)
def collatzLengthImpl
collatzLengthImpl = { long n, int steps = 0 ->
    if (n == 1) return steps
    if (n % 2 == 0) {
        collatzLengthImpl.trampoline((long)(n / 2), steps + 1)
    } else {
        collatzLengthImpl.trampoline((long)(3 * n + 1), steps + 1)
    }
}
def collatzLength = collatzLengthImpl.trampoline()

println "\n=== Collatz Sequence Lengths ==="
[1, 2, 3, 7, 27, 100, 1000, 10000, 77031].each { n ->
    println "  collatz(${String.valueOf(n).padRight(6)}) takes ${collatzLength(n)} steps"
}

Output

gcd(48, 18)    = 6
gcd(100, 75)   = 25
gcd(1071, 462) = 21

lcm(12, 18) = 36
lcm(7, 13)  = 91

gcd(huge1, huge2) = 9000000000900000000090

=== Collatz Sequence Lengths ===
  collatz(1     ) takes 0 steps
  collatz(2     ) takes 1 steps
  collatz(3     ) takes 7 steps
  collatz(7     ) takes 16 steps
  collatz(27    ) takes 111 steps
  collatz(100   ) takes 25 steps
  collatz(1000  ) takes 111 steps
  collatz(10000 ) takes 29 steps
  collatz(77031 ) takes 350 steps

What happened here: The Euclidean algorithm for GCD is a perfect fit for trampoline – it’s already tail-recursive by nature. The Collatz sequence is particularly interesting because the number of steps is unpredictable. The number 77031 takes 350 steps before reaching 1, with intermediate values that can be extremely large. Without trampoline, such deep recursion would crash. The GCD calculation on 30-digit numbers demonstrates that trampoline handles BigInteger arithmetic without any issues.

Example 9: State Machine with Trampoline

What we’re doing: Implementing a simple state machine where each state is a trampoline closure that transitions to the next state.

Example 9: State Machine

// State machine for processing a simple order workflow
def stateNew
def statePaid
def stateShipped
def stateDelivered

stateNew = { Map order, List log = [] ->
    log = log + "Order #${order.id}: Created"
    if (order.payment) {
        statePaid.trampoline(order, log)
    } else {
        log + "STOPPED: No payment received"
    }
}.trampoline()

statePaid = { Map order, List log ->
    log = log + "Order #${order.id}: Payment received (\$${order.amount})"
    if (order.warehouse) {
        stateShipped.trampoline(order, log)
    } else {
        log + "STOPPED: Warehouse unavailable"
    }
}.trampoline()

stateShipped = { Map order, List log ->
    log = log + "Order #${order.id}: Shipped via ${order.carrier}"
    if (order.delivered) {
        stateDelivered.trampoline(order, log)
    } else {
        log + "IN TRANSIT: Awaiting delivery"
    }
}.trampoline()

stateDelivered = { Map order, List log ->
    log + "Order #${order.id}: Delivered! Complete."
}.trampoline()

// Test different order scenarios
println "=== Scenario 1: Full journey ==="
def order1 = [id: 101, payment: true, amount: 99.99,
              warehouse: true, carrier: 'FedEx', delivered: true]
stateNew(order1).each { println "  ${it}" }

println "\n=== Scenario 2: No payment ==="
def order2 = [id: 102, payment: false, amount: 49.99]
stateNew(order2).each { println "  ${it}" }

println "\n=== Scenario 3: In transit ==="
def order3 = [id: 103, payment: true, amount: 149.99,
              warehouse: true, carrier: 'UPS', delivered: false]
stateNew(order3).each { println "  ${it}" }

println "\n=== Scenario 4: Warehouse issue ==="
def order4 = [id: 104, payment: true, amount: 29.99, warehouse: false]
stateNew(order4).each { println "  ${it}" }

Output

=== Scenario 1: Full journey ===
  Order #101: Created
  Order #101: Payment received ($99.99)
  Order #101: Shipped via FedEx
  Order #101: Delivered! Complete.

=== Scenario 2: No payment ===
  Order #102: Created
  STOPPED: No payment received

=== Scenario 3: In transit ===
  Order #103: Created
  Order #103: Payment received ($149.99)
  Order #103: Shipped via UPS
  IN TRANSIT: Awaiting delivery

=== Scenario 4: Warehouse issue ===
  Order #104: Created
  Order #104: Payment received ($29.99)
  STOPPED: Warehouse unavailable

What happened here: Each state is a trampoline closure that either transitions to the next state or returns a final result. The log accumulator tracks the journey. This pattern is powerful for workflows, protocol handlers, and game logic where you have many states that transition between each other. Because each state uses .trampoline() for transitions, you could have workflows with thousands of state transitions without any stack concerns. It’s essentially a state machine implemented with mutual recursion – clean, readable, and stack-safe.

Example 10: Binary Search with Trampoline

What we’re doing: Implementing binary search as a tail-recursive trampoline closure – a practical algorithm that benefits from stack safety.

Example 10: Binary Search

// Tail-recursive binary search
def binarySearch
binarySearch = { List sortedList, target, int low = 0, int high = -1 ->
    if (high == -1) high = sortedList.size() - 1

    if (low > high) return -1  // Not found

    int mid = (low + high).intdiv(2)
    def midVal = sortedList[mid]

    if (midVal == target) return mid
    if (midVal < target) {
        binarySearch.trampoline(sortedList, target, mid + 1, high)
    } else {
        binarySearch.trampoline(sortedList, target, low, mid - 1)
    }
}.trampoline()

// Test with a sorted list
def data = (1..1000).toList()

println "=== Binary Search ==="
println "Find 500:  index ${binarySearch(data, 500)}"
println "Find 1:    index ${binarySearch(data, 1)}"
println "Find 1000: index ${binarySearch(data, 1000)}"
println "Find 999:  index ${binarySearch(data, 999)}"
println "Find 1001: index ${binarySearch(data, 1001)} (not found)"

// Search in strings
def words = ['apple', 'banana', 'cherry', 'date', 'elderberry',
             'fig', 'grape', 'honeydew', 'kiwi', 'lemon'].sort()
println "\n=== String Search ==="
println "Find 'grape':  index ${binarySearch(words, 'grape')}"
println "Find 'mango':  index ${binarySearch(words, 'mango')} (not found)"
println "Find 'apple':  index ${binarySearch(words, 'apple')}"

// Tail-recursive power function (exponentiation by squaring)
def power
power = { BigInteger base, int exp, BigInteger acc = 1G ->
    if (exp == 0) return acc
    if (exp % 2 == 0) {
        power.trampoline(base * base, exp.intdiv(2), acc)
    } else {
        power.trampoline(base, exp - 1, acc * base)
    }
}.trampoline()

println "\n=== Fast Exponentiation ==="
println "2^10  = ${power(2, 10)}"
println "3^20  = ${power(3, 20)}"
println "2^100 = ${power(2, 100)}"
println "7^50 has ${power(7, 50).toString().size()} digits"

Output

=== Binary Search ===
Find 500:  index 499
Find 1:    index 0
Find 1000: index 999
Find 999:  index 998
Find 1001: index -1 (not found)

=== String Search ===
Find 'grape':  index 6
Find 'mango':  index -1 (not found)
Find 'apple':  index 0

=== Fast Exponentiation ===
2^10  = 1024
3^20  = 3486784401
2^100 = 1267650600228229401496703205376
7^50 has 43 digits

What happened here: Binary search is naturally tail-recursive – each step either returns a result or recurses into one half of the array. While binary search’s O(log n) depth won’t overflow the stack for practical list sizes, the trampoline pattern ensures it’s safe for any size. The exponentiation-by-squaring algorithm is another beautiful fit – it computes base^exp in O(log n) steps, and the tail-recursive version with trampoline can handle enormous exponents without stack concerns.

Example 11 (Bonus): Flattening Deeply Nested Structures

What we’re doing: Recursively flattening a deeply nested list structure – a real-world scenario where regular recursion can fail.

Example 11: Deep Flattening

// Build a deeply nested structure
def buildNested
buildNested = { int depth, value ->
    if (depth == 0) return value
    [buildNested(depth - 1, value)]
}

// Create moderately nested structure for demo
def nested = buildNested(5, 'deep')
println "Nested 5 levels: ${nested}"

// Tail-recursive flatten using a work stack (iterative accumulation)
def flattenDeep
flattenDeep = { List items, List remaining = [], List acc = [] ->
    if (items.isEmpty()) {
        if (remaining.isEmpty()) return acc
        flattenDeep.trampoline(remaining.head(), remaining.tail(), acc)
    } else {
        def first = items[0]
        def rest = items.size() > 1 ? items.subList(1, items.size()) : []
        if (first instanceof List) {
            // Push rest onto remaining, process first
            def newRemaining = rest.isEmpty() ? remaining : [rest] + remaining
            flattenDeep.trampoline(first, newRemaining, acc)
        } else {
            flattenDeep.trampoline(rest, remaining, acc + first)
        }
    }
}.trampoline()

// Test flattening
def test1 = [[1, 2], [3, [4, 5]], [6]]
println "Flatten ${test1}: ${flattenDeep(test1)}"

def test2 = [1, [2, [3, [4, [5]]]]]
println "Flatten ${test2}: ${flattenDeep(test2)}"

def test3 = [[[['a']], [['b']]], [[['c']]]]
println "Flatten ${test3}: ${flattenDeep(test3)}"

// Empty and edge cases
println "Flatten []: ${flattenDeep([])}"
println "Flatten [[],[[]]]: ${flattenDeep([[], [[]]])}"

// Compare with Groovy's built-in flatten
assert flattenDeep(test1) == test1.flatten()
assert flattenDeep(test2) == test2.flatten()
assert flattenDeep(test3) == test3.flatten()
println "\nAll results match Groovy's built-in flatten()!"

Output

Nested 5 levels: [[[[[deep]]]]]
Flatten [[1, 2], [3, [4, 5]], [6]]: [1, 2, 3, 4, 5, 6]
Flatten [1, [2, [3, [4, [5]]]]]: [1, 2, 3, 4, 5]
Flatten [[[[a]], [[b]]], [[[c]]]]: [a, b, c]
Flatten []: []
Flatten [[],[[]]]: []

All results match Groovy's built-in flatten()!

What happened here: Flattening deeply nested structures is tricky to make tail-recursive because you need to process both the first element and the rest of the list. The solution uses a “remaining work” list as a stack – when we encounter a nested list, we push the rest of the current level onto the remaining stack and recurse into the nested list. This transforms what would be a tree-recursive problem into a linear, tail-recursive one. While Groovy’s built-in flatten() handles most cases, this trampoline version can handle arbitrarily deep nesting without stack overflow.

Trampoline vs Memoize – When to Use Which

Both trampoline and memoize are closure optimization techniques, but they solve completely different problems:

AspectTrampolineMemoize
Problem solvedStack overflow from deep recursionRedundant computation of same arguments
How it worksConverts recursion to iterationCaches results in a map
Memory impactConstant stack usageCache grows with unique arguments
Speed impactSlight overhead per bounceMassive speedup on repeated calls
RequirementMust be tail-recursiveMust be a pure function
Best forDeep recursion, large inputsExpensive computations with repeated args

Can you use both together? Yes! Memoize the expensive computation, and trampoline the recursion depth. For Fibonacci, memoization eliminates redundant subproblems while trampoline prevents stack overflow. In practice, though, once you make Fibonacci tail-recursive (with accumulators), it no longer has overlapping subproblems, so memoization isn’t needed. The choice depends on the structure of your specific algorithm.

Converting Recursion to Tail Recursion

The hardest part of using trampoline is converting your recursion to tail-recursive form. Here’s the recipe:

Step-by-Step Conversion

Converting to Tail Recursion

// STEP 1: Start with a regular recursive function
def sumListRegular = { List items ->
    if (items.isEmpty()) return 0
    items[0] + call(items.tail())  // NOT tail-recursive
}

// STEP 2: Add an accumulator parameter with a default value
def sumListTail = { List items, long acc = 0 ->
    if (items.isEmpty()) return acc
    call(items.tail(), acc + items[0])  // Tail-recursive!
}

// STEP 3: Apply trampoline
def sumListTrampoline
sumListTrampoline = { List items, long acc = 0 ->
    if (items.isEmpty()) return acc
    sumListTrampoline.trampoline(items.tail(), acc + items[0])
}.trampoline()

// All three produce the same result
def data = (1..100).toList()
println "Regular:    ${sumListRegular(data)}"
println "Tail:       ${sumListTail(data)}"
println "Trampoline: ${sumListTrampoline(data)}"

// The conversion pattern:
println """
=== Conversion Recipe ===
1. Identify the operation AFTER the recursive call (e.g., +, *, append)
2. Add an accumulator parameter (with default = identity value)
   - For addition: acc = 0
   - For multiplication: acc = 1
   - For list building: acc = []
   - For string building: acc = ''
3. Move the operation BEFORE the recursive call
   - Instead of: items[0] + recurse(rest)
   - Write:      recurse(rest, acc + items[0])
4. In the base case, return the accumulator instead of the identity value
5. Apply .trampoline() to the closure and use .trampoline() for recursive calls
"""

Output

Regular:    5050
Tail:       5050
Trampoline: 5050

=== Conversion Recipe ===
1. Identify the operation AFTER the recursive call (e.g., +, *, append)
2. Add an accumulator parameter (with default = identity value)
   - For addition: acc = 0
   - For multiplication: acc = 1
   - For list building: acc = []
   - For string building: acc = ''
3. Move the operation BEFORE the recursive call
   - Instead of: items[0] + recurse(rest)
   - Write:      recurse(rest, acc + items[0])
4. In the base case, return the accumulator instead of the identity value
5. Apply .trampoline() to the closure and use .trampoline() for recursive calls

This conversion recipe works for most single-recursion problems. For problems with branching recursion (like tree traversal where you visit both left and right children), you need to use a work stack or continuation-passing style, as we saw in Example 11.

Common Pitfalls

Pitfall 1: Non-Tail-Recursive Calls

Pitfall 1: Not Tail-Recursive

// BAD: This is NOT tail-recursive - multiplication happens AFTER the recursive call
def factorialBad
factorialBad = { int n ->
    if (n <= 1) return 1G
    n * factorialBad.trampoline(n - 1)  // WON'T WORK CORRECTLY!
}.trampoline()

// This will produce wrong results or errors because
// n * trampoline(...) is NOT a tail call

// GOOD: Move the multiplication into the accumulator
def factorialGood
factorialGood = { int n, BigInteger acc = 1G ->
    if (n <= 1) return acc
    factorialGood.trampoline(n - 1, n * acc)  // Tail call!
}.trampoline()

println "Correct factorial(10): ${factorialGood(10)}"

Output

Correct factorial(10): 3628800

Pitfall 2: Forgetting to Call .trampoline() on Both Sides

Pitfall 2: Missing Trampoline Calls

// BAD: Only outer .trampoline() - recursive calls still use the stack
def badCountdown
badCountdown = { int n ->
    if (n == 0) return "Done"
    badCountdown(n - 1)  // Direct call - still recursive!
}.trampoline()

// GOOD: Both outer .trampoline() AND inner .trampoline()
def goodCountdown
goodCountdown = { int n ->
    if (n == 0) return "Done"
    goodCountdown.trampoline(n - 1)  // Returns a thunk!
}.trampoline()

println "Good countdown(1000): ${goodCountdown(1000)}"

// Remember: closure.trampoline(args) inside = return a thunk
//           closure.trampoline() on definition = wrap in trampoline loop

Output

Good countdown(1000): Done

Pitfall 3: Trampoline Overhead on Short Recursions

Pitfall 3: Overhead for Small Recursions

// For small recursion depths, trampoline adds overhead
def regularFact = { int n -> (1..n).inject(1G) { acc, val -> acc * val } }

def trampolineFact
trampolineFact = { int n, BigInteger acc = 1G ->
    if (n <= 1) return acc
    trampolineFact.trampoline(n - 1, n * acc)
}.trampoline()

// Benchmark
def start1 = System.nanoTime()
100000.times { regularFact(20) }
def time1 = (System.nanoTime() - start1) / 1_000_000

def start2 = System.nanoTime()
100000.times { trampolineFact(20) }
def time2 = (System.nanoTime() - start2) / 1_000_000

println "Regular (inject): ${time1}ms for 100K calls"
println "Trampoline:       ${time2}ms for 100K calls"
println "\nRule: Use trampoline for DEEP recursion (1000+ levels)"
println "      Use iteration for SHALLOW recursion"

Output

Regular (inject): 156ms for 100K calls
Trampoline:       298ms for 100K calls

Rule: Use trampoline for DEEP recursion (1000+ levels)
      Use iteration for SHALLOW recursion

The trampoline has per-bounce overhead – creating thunk objects and the loop machinery. For shallow recursion (under 100 levels), this overhead outweighs the benefit. Reserve trampoline for algorithms where recursion depth is large or unpredictable.

Conclusion

We’ve explored Groovy trampoline from simple countdowns to mutual recursion, state machines, and dynamic programming. The .trampoline() method is Groovy’s answer to the JVM’s lack of tail-call optimization – it lets you write recursive code that executes iteratively, with constant stack usage regardless of recursion depth.

The pattern is always the same: make your closure tail-recursive (use accumulators), call .trampoline() on the closure definition, and use closureName.trampoline(args) for recursive calls inside. That’s it. Three changes and your recursion becomes stack-safe.

Combined with memoization for caching and delegation for flexible scoping, trampoline completes Groovy’s trio of closure optimization techniques.

Summary

  • .trampoline() converts tail-recursive closures into iterative execution – no stack overflow
  • Your closure must be tail-recursive – the recursive call must be the very last operation
  • Use accumulators to convert regular recursion to tail recursion
  • Trampoline works for mutual recursion (two closures calling each other)
  • Use trampoline for deep recursion (1000+ levels); for shallow recursion, the overhead isn’t worth it

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 Elvis Operator – Default Values

Frequently Asked Questions

What is trampoline() in Groovy?

trampoline() is a method on Groovy closures that enables stack-safe recursion. It converts a tail-recursive closure into an iterative loop internally. Instead of each recursive call adding a frame to the call stack, the call returns a ‘thunk’ (deferred computation) that a trampoline loop evaluates iteratively. This means you can recurse millions of times without getting a StackOverflowError.

What is the difference between trampoline and memoize in Groovy?

They solve different problems. trampoline() prevents StackOverflowError by converting recursion to iteration – it doesn’t affect computation speed. memoize() caches results to avoid redundant computation – it doesn’t affect stack depth. trampoline requires tail-recursive closures, while memoize requires pure functions. You can sometimes use both together, though tail-recursive closures typically don’t have overlapping subproblems that would benefit from memoization.

Why does trampoline require tail recursion?

Trampoline works by replacing the recursive call with a thunk (a deferred computation object) that the trampoline loop evaluates. If the recursive call isn’t the last operation (e.g., n * recurse(n-1)), the multiplication needs the recursive result before it can complete – it can’t be deferred as a thunk. Only when the recursive call IS the last operation can it be safely replaced with a thunk. That’s why you need to convert to tail recursion using accumulators.

Does Groovy support tail-call optimization natively?

No. The JVM does not support tail-call optimization (TCO) at the bytecode level, so neither Java nor Groovy can optimize tail calls automatically. Groovy trampoline() method is a manual workaround that achieves the same effect – stack-safe recursion – through a different mechanism (converting recursion to iteration via thunks). Some other JVM languages like Scala have compiler-level TCO support via the @tailrec annotation.

Can I use trampoline with mutual recursion in Groovy?

Yes! Trampoline works perfectly with mutual recursion – when two or more closures call each other. Apply .trampoline() to each closure’s definition, and use closureName.trampoline(args) for each cross-closure call. The trampoline loop handles the bouncing between closures iteratively. This is particularly useful for state machines, parsers, and protocol handlers where multiple functions naturally call each other.

Previous in Series: Groovy Memoization – Cache Function Results

Next in Series: Groovy Elvis Operator – Default Values

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 *