DEV Community

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

Posted on

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

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

Top comments (0)