5 min read
Variables Don't Change, They Get Rebound
Why Elixir Bindings Make Concurrent Code Predictable

If everything in Elixir returns a value, the next question comes naturally:

If values keep flowing through expressions… what are variables, then?

Because they don’t behave the way you expect.

In most languages, a variable is a container. You put a value in it. You change that value. The container stays the same.

Elixir variables are not containers. They are bindings.

That distinction sounds subtle. It isn’t. It’s one of the reasons Elixir code stays predictable under pressure.

Variables vs. Bindings

Start with something that looks familiar:

x = 1
x = 2

If you’re coming from Ruby, JavaScript, or Python, this looks like mutation. Assign one value, then replace it.

But that’s not what’s happening.

In Elixir, = is not assignment. It’s pattern matching.

The first line binds the name x to the value 1. The second line binds the name x to the value 2.

Those are two separate bindings, created at two different moments in time.

Nothing changed. A new binding was introduced.

The old value still exists. It just isn’t referenced by x anymore.

🎯 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.

Why This Matters More Than Immutability

People often summarize this as “Elixir is immutable,” which is true but incomplete.

The more important idea is this:

Names don’t own values. They point at them.

That single rule explains a lot of Elixir’s behavior.

It explains why you can’t “update” data in place. It explains why functions are easier to reason about. It explains why concurrency works without locks.

Once a value exists, it never changes. You can only create a new value and bind a name to it.

How Rebinding Actually Feels in Practice

This pattern appears everywhere in Elixir:

socket =
  socket
  |> assign(:count, socket.assigns.count + 1)

At first glance, this looks like mutation.

It isn’t.

What’s really happening:

  • The old socket value still exists
  • assign/3 returns a new socket
  • The name socket is rebound to that new value

Same name. New value. Nothing mutated.

You’ll see this pattern with multiple rebindings too:

def update(assigns, socket) do
  socket = assign(socket, :count, assigns.count + 1)
  socket = assign(socket, :updated_at, DateTime.utc_now())
  {:ok, socket}
end

Each line creates a new socket. The name gets rebound twice. The original socket still exists somewhere in memory, but we’re working with progressively newer versions.

This is why Elixir code often reuses the same variable name. It’s not hiding state changes. It’s making value flow explicit.

Why This Makes Concurrency Boring (In a Good Way)

The quiet payoff of bindings:

If values never change, then:

  • Two processes can’t accidentally change the same data
  • There’s no shared mutable state to coordinate
  • You don’t need defensive copying or locks

Each process works with its own values. If it needs new data, it creates new values.

This is why Elixir can run thousands of processes without elaborate synchronization machinery.

The language design removes an entire category of problems before you can even write them.

A Common Mistake to Watch For

Developers new to Elixir often fight bindings by trying to “update” things step by step.

You’ll see code like this:

result = calculate(a)
result = adjust(result)
result = finalize(result)

This works, but it misses the point.

What you’re really expressing is a transformation pipeline:

result =
  a
  |> calculate()
  |> adjust()
  |> finalize()

Same idea. Clearer intent.

Bindings are there to name moments in a transformation, not to track evolving state.

What to Practice Today

Build this concrete habit:

When you see a variable name reused in Elixir, don’t read it as “this thing changed.”

Read it as: “The name now refers to a newer value.”

That framing changes how you debug, how you refactor, and how you reason about code.

If you ever feel tempted to ask, “What’s the current value of this variable?” Pause.

In Elixir, the better question is: “What expression produced the value this name is bound to right now?”

Follow the expressions. The bindings will make sense.

What Comes Next

If variables don’t change, and values don’t change, then the next obvious question is:

How does Elixir handle complex decisions without mutable state?

That’s where pattern matching really shows its power—not just as syntax, but as a way to move control flow into data and function boundaries.

That’s where we’re going next.

See you in the next chapter.

— Paulo Valim & Bruce Tate


Want to Learn Elixir the Way It's Actually Designed?

This post is part of our Elixir series based on Bruce Tate's structured course, where each concept builds on the previous one: expressions, bindings, and soon, pattern matching.

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.