State Management in Elixir: Processes, Agents, and GenServers in Action
Rafael Andrade

Rafael Andrade @actor-dev

About: I'm Rafael the Actor Dev and I like to talk about Actor Models, Brighter, Elixir & Design Patterns Eu me chamo Rafael o Actor Dev e eu gosto de falar sobre Actor Models, Brighter, Elixir & Design Pat

Location:
London, UK
Joined:
Jan 24, 2025

State Management in Elixir: Processes, Agents, and GenServers in Action

Publish Date: Jun 27
0 0

Introduction

When I first learned Elixir, I was wondering how to manage state. Unlike imperative languages with mutable global variables, Elixir’s immutable data model and concurrency-driven design (via the BEAM VM) require a different approach. In this article, I’ll explore how state is handled in Elixir.

Context: BEAM VM and Concurrency

Elixir runs on the BEAM virtual machine, designed for high concurrency and fault tolerance. Inspired by the Actor Model, BEAM treats processes as lightweight entities that communicate via message passing. Because data is immutable, state changes are achieved by creating new values rather than modifying existing ones. This ensures thread safety and simplifies concurrent programming.

Recursive Loops

The simplest way to maintain state is by implementing a recursive loop. Here’s an example:

defmodule StatefulMap do
  def start do
    spawn(fn -> loop(%{}) end)
  end

  def loop(current) do
    new =
      receive do
        message -> process(current, message)
      end

    loop(new)
  end

  def put(pid, key, value) do
    send(pid, {:put, key, value})
  end

  def get(pid, key) do
    send(pid, {:get, key, self})

    receive do
      {:response, value} -> value
    end
  end

  defp process(current, {:put, key, value}) do
    Map.put(current, key, value)
  end

  defp process(current, {:get, key, caller}) do
    send(caller, {:response, Map.get(current, key)})
    current
  end
end
Enter fullscreen mode Exit fullscreen mode

Usage would be:

pid = StatefulMap.start() # PID<0.63.0>
StatefulMap.put(pid, :hello, :world)
StatefulMap.get(pid, :hello) # :world
Enter fullscreen mode Exit fullscreen mode

Agents

Another option is the Agent; this module allows you to easily share state between different processes or among the same process over time.

A sample implementation:

defmodule Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end
Enter fullscreen mode Exit fullscreen mode

Usage would be:

Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}

Counter.value()
#=> 0

Counter.increment()
#=> :ok

Counter.increment()
#=> :ok

Counter.value()
#=> 2
Enter fullscreen mode Exit fullscreen mode

To start it, it's recommended to use a supervisor

children = [
  {Counter, 0}
]

Supervisor.start_link(children, strategy: :one_for_all)
Enter fullscreen mode Exit fullscreen mode

GenServer

The most classic option is a GenServer behaviour (it's similar to interface in .NET/Java), allows you to manage your state with sync and async requests

Key callbacks:

  • init/1 -> when the actor is started.
  • handle_call/2 -> Sync request (e.g., expecting a response).
  • handle_cast/3 -> Async request (e.g., fire-and-forget).

Here is a sample of GenServer

defmodule Stack do
  use GenServer

  # Callbacks

  @impl true
  def init(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

  @impl true
  def handle_cast({:push, element}, state) do
    new_state = [element | state]
    {:noreply, new_state}
  end
end
Enter fullscreen mode Exit fullscreen mode

Usage:

# Start the server
{:ok, pid} = GenServer.start_link(Stack, "hello,world")

# This is the client
GenServer.call(pid, :pop)
#=> "hello"

GenServer.cast(pid, {:push, "elixir"})
#=> :ok

GenServer.call(pid, :pop)
#=> "elixir"
Enter fullscreen mode Exit fullscreen mode

Conclusion

Elixir’s state management relies on processes and immutability. Recursive loops provide foundational control, Agent simplifies shared state, and GenServer offers robust concurrency with supervision integration. Each tool serves distinct use cases, from simple counters to complex state logic.

Reference

Working with State and Elixir Processes

GenServer

Agent

Comments 0 total

    Add comment