Back to the field notes

It’s messages all the way down

A closer look at processes, mailboxes, and the beautifully simple idea behind the BEAM.

In this field note

Elixir processes have their own state and communicate by sending messages. A message is data: an atom, a tuple, a map, or another Erlang term.

That simple model is worth looking at before introducing supervisors, behaviors, and the rest of OTP.

A process with a mailbox

This module keeps a counter. The recursive call carries the next state; receive selects a matching message from the mailbox.

defmodule Counter do
  def start(initial \\ 0) do
    spawn(fn -> loop(initial) end)
  end

  defp loop(value) do
    receive do
      :increment ->
        loop(value + 1)

      {:get, caller, reference} ->
        send(caller, {reference, value})
        loop(value)
    end
  end
end

There is no shared mutable variable. Each iteration receives a value and decides what the next value should be.

Ask a question, match the answer

Use a fresh reference to correlate the reply with this request:

counter = Counter.start()
send(counter, :increment)

reference = make_ref()
send(counter, {:get, self(), reference})

receive do
  {^reference, value} -> IO.inspect(value)
after
  1_000 -> IO.puts("No reply within one second")
end

The pinned reference ensures this receive only accepts the response to our request. A timeout ensures the caller does not wait forever.

This tiny example prints 1: messages from the same sender to the same receiver preserve their order.

What the toy version leaves out

This counter has no supervision or monitoring. Unmatched messages stay in its mailbox. If the process crashes, callers only learn indirectly through a timeout.

Those are real design problems, and they are exactly the kind of machinery OTP helps you manage. A GenServer provides a standard structure for the state loop and request/reply interaction. Supervisors provide restart strategies.

A useful mental model

Think about a process as a small machine with a mailbox. Its protocol determines what other processes can ask it to do. Its state is an implementation detail.

That separation is useful even outside the BEAM. It makes a good question to ask of a module, service, or hardware component: what messages cross the boundary, and who owns the state?

More experiments. More rabbit holes.

Follow along via RSS Share on X