OTP — GenServer, Supervisor, and Agent

Elixir OTP behaviours — GenServer, Supervisor, Agent, and Task for building reliable concurrent systems

OTP (Open Telecom Platform) is Erlang’s framework for building fault-tolerant systems. Elixir provides GenServer, Supervisor, Agent, and Task as the primary building blocks.

GenServer

A GenServer is a process that holds state and handles synchronous (call) and asynchronous (cast) requests.

Defining a GenServer

defmodule Counter do
  use GenServer

  # --- Client API ---

  def start_link(initial \\ 0) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def inc(pid \\ __MODULE__) do
    GenServer.cast(pid, :inc)
  end

  def dec(pid \\ __MODULE__) do
    GenServer.cast(pid, :dec)
  end

  def get(pid \\ __MODULE__) do
    GenServer.call(pid, :get)
  end

  # --- Server Callbacks ---

  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_call(:get, _from, count) do
    {:reply, count, count}
  end

  @impl true
  def handle_cast(:inc, count) do
    {:noreply, count + 1}
  end

  @impl true
  def handle_cast(:dec, count) do
    {:noreply, count - 1}
  end
end

Using it

Counter.start_link(0)   # {:ok, pid}
Counter.inc()            # :ok (async)
Counter.inc()            # :ok (async)
Counter.get()            # 2 (sync)

handle_call return values

Return Meaning
{:reply, data, new_state} Reply to caller, update state
{:reply, data, new_state, timeout} Same + set inactivity timeout
{:noreply, new_state} Don’t reply yet (use GenServer.reply/2 later)
{:stop, reason, reply, new_state} Stop gracefully, send reply
{:stop, reason, new_state} Stop gracefully, no reply

handle_cast return values

Return Meaning
{:noreply, new_state} Update state, continue
{:noreply, new_state, timeout} Same + set inactivity timeout
{:stop, reason, new_state} Stop gracefully

Supervisor

Supervisors monitor child processes and restart them when they crash.

Static supervisor

defmodule MyApp.Supervisor do
  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    children = [
      {Counter, 0},
      {MyApp.Worker, []}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Dynamic supervisor

For processes that are started/stopped at runtime:

# In your application supervision tree:
children = [
  {DynamicSupervisor, strategy: :one_for_one, name: MyApp.DynamicSupervisor}
]

# Starting a child dynamically
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {MyWorker, arg})

# Stopping a child
DynamicSupervisor.terminate_child(MyApp.DynamicSupervisor, pid)

Restart strategies

Strategy Description
:one_for_one Only the crashed process is restarted
:one_for_all All children are terminated and restarted
:rest_for_one Crashed process + all started after it are restarted

Restart options

Option Meaning
:permanent Always restart (default)
:temporary Never restart
:transient Restart only on abnormal exit

Agent

Simpler than GenServer — just holds state with get/update functions.

# Start an agent
{:ok, pid} = Agent.start_link(fn -> 0 end)

# Or with a name
Agent.start_link(fn -> %{} end, name: :cache)

# Get state
Agent.get(:cache, & &1)                    # => %{}
Agent.get(:cache, fn state -> state[:key] end)

# Update state
Agent.update(:cache, fn state -> Map.put(state, :key, "value") end)

# Get and update
Agent.get_and_update(:cache, fn state ->
  {state[:count], Map.update(state, :count, 1, &(&1 + 1))}
end)

Task

Run a computation in a separate process. Good for one-off work.

# Fire and forget
Task.start(fn -> do_something_slow() end)

# Await result (blocks up to timeout)
task = Task.async(fn -> fetch_from_api() end)
result = Task.await(task, 5000)  # 5s timeout

# Multiple tasks
tasks = Enum.map(urls, &Task.async(fn -> HTTP.get(&1) end))
results = Task.await_many(tasks, 10_000)

# Parallel stream
Task.async_stream(collection, &process/1, max_concurrency: 4)
|> Enum.to_list()

Application supervision tree

In application.ex:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyApp.PubSub,
      {DynamicSupervisor, strategy: :one_for_one, name: MyApp.DynamicSupervisor},
      MyApp.Web.Endpoint
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end