5 min read
Recursion as a Design Pattern
How Accumulators Turn Recursive List Processing into Clear, Stack-Safe Elixir

The first time you need to process a list in Elixir, you look for something familiar. A while loop. A counter you increment. Some kind of for-each construct you can reach for. You don’t find it, not in the form you’re used to. What you find instead is recursion.

Most developers treat recursion as a technique, something you pull out for algorithms and puzzles. In Elixir it’s different. Recursion is the natural shape of list processing. And once you understand the accumulator pattern, you stop looking for loops and start thinking about what to carry forward.

Two Recursive Styles

There’s a simpler form you’ll probably write first:

def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)

This version matches the shape of the data. The base case says an empty list sums to zero. The recursive case takes the first element and adds it to the sum of the rest.

But look at the structure. The last thing this function does is not call itself. It’s head + sum(tail), which means the function has to wait for sum(tail) to return before it can add anything. Every pending addition sits on the stack until the base case fires.

For a short list that’s fine. For a long one, that’s a growing stack with nowhere to go.

🎯 Join Groxio's Newsletter

Weekly lessons on Elixir, system design, and AI-assisted development - plus stories from our training and mentoring sessions.

We respect your privacy. No spam, unsubscribe anytime.

The Accumulator Pattern

The solution is to carry the result forward instead of building it on the way back. That’s the accumulator: a value passed through each recursive call, updated as you go.

Think of it like a relay race. In the first version, each runner waits for the person behind them before they can move. In the accumulator version, each runner carries the current total and hands it off. The last runner already holds the final answer when they cross the line.

def sum(list), do: sum(list, 0)

defp sum([], acc), do: acc
defp sum([head | tail], acc), do: sum(tail, acc + head)

The base case no longer says “an empty list sums to zero.” It says “when there’s nothing left to process, return what was built so far.” The recursive case updates the accumulator immediately, before the recursive call.

This is the mental model that matters: the accumulator is not a global variable, and it is not a hidden mutable box. It is the state of the computation, made explicit and passed forward.

The first version says, “I’ll know what this means when the rest finishes.” The second says, “I already know what this step contributes, so I’ll carry that forward now.”

Bruce names the property this enables:

“Tail recursion happens when the very last thing that you do in a function is to call your function.”

  • Bruce Tate

When the recursive call is last, the compiler can optimize it into a loop under the hood. There’s no extra work waiting after each call, so no new frame gets added to the stack. This is what Bruce means when he describes what makes that safe:

“We are processing this whole program without ever having to have one memory changed in value because my call stack is managing all of that for me.”

  • Bruce Tate

The accumulator holds the growing result as a function argument. That’s not a design compromise. It’s how Elixir keeps state moving forward without ever mutating anything in place.

Hiding the Accumulator

Notice the public function takes one argument. The recursive helper takes two. Callers don’t know the accumulator exists, and they shouldn’t.

The public sum/1 creates a clean entry point and sets the initial accumulator to zero. The private sum/2 owns the recursive machinery. This isn’t just tidiness. It’s about owning the API surface. The caller should see a function that takes a list and returns a sum. How that sum is computed is your problem, not theirs.

What This Pattern Becomes

You’ve built something specific: a function that walks a list one element at a time, applies each element to an accumulator, and returns the accumulated result when the list is empty. Bruce gives that pattern its proper name:

“All that we’re doing is taking one small step forward, applying one item to the accumulator at a time. And that’s something that you’re going to see later that’s called reduce.”

  • Bruce Tate

When you use Enum.reduce, this is the pattern under the hood. The function you pass to reduce is the “do one unit of work and update the accumulator” step. Understanding the accumulator now means reduce won’t feel like a black box. It’ll be a name for something you already know how to build.

So when recursion starts to feel strange, don’t ask how to make it look more like a loop. Ask what your base case must return, and whether your recursive step should wait for meaning on the way back or carry meaning forward as an accumulator. Once that question feels natural, recursion stops feeling like a workaround and starts feeling like the obvious way to work with lists.

That’s where we’re going next: the Enum module and the higher-order functions built on exactly this foundation.


📚 Master the Mental Models Behind Elixir Recursion

This comes from our Learning Elixir course, where Bruce teaches the mental models behind real-world scenarios and production architecture decisions. Learn how recursion, accumulators, and reduction patterns make state explicit without mutation. Available via monthly subscription - try it for one month.

See you in the next chapter.

  • Paulo & Bruce
Bruce Tate's avatar
Bruce Tate
System architecture expert and author of 10+ Elixir books.
Paulo Valim's avatar
Paulo Valim
Full-stack Elixir developer and educator teaching modern Elixir and AI-assisted development.