Elixir Fundamentals: Functional Programming and OTP · Processes, OTP and Fault Tolerance
GenServer and Supervisors: OTP in Practice
Keep state in a GenServer with call (synchronous) and cast (asynchronous) messages, start it under a Supervisor, and watch the supervisor restart it after a crash. This is the backbone of every Elixir and Phoenix app.
What you'll build
A rate-limited shopping cart service: a GenServer holding carts in its state, a public API that hides the message passing, and a Supervisor that restarts the service if it crashes.
The idea
OTP is the set of libraries and design principles that come with Erlang/Elixir. Two building blocks matter most:
GenServer: a process that holds state and handles messages through callbacks:init/1sets the initial state,handle_call/3answers synchronous requests (GenServer.call: the caller waits for a reply),handle_cast/2handles asynchronous ones (GenServer.cast: fire and forget),handle_info/2handles other messages (timers, monitors).
Supervisor: starts child processes and restarts them according to a strategy (:one_for_onerestarts only the crashed child) when they crash.
Wrap the message passing in a client API (Cart.add(item)), so callers never see call/cast directly. Name the process (name: __MODULE__) so it can be found without passing pids.
In a Mix project, your application's supervision tree is started in lib/my_app/application.ex; Phoenix apps are trees of supervisors and GenServers.
The code
defmodule Cart do
use GenServer
# Client API: what the rest of the app calls
def start_link(_opts), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def add(user, item), do: GenServer.cast(__MODULE__, {:add, user, item})
def items(user), do: GenServer.call(__MODULE__, {:items, user})
def crash, do: GenServer.cast(__MODULE__, :crash)
# Server callbacks: run inside the GenServer process
@impl true
def init(state) do
IO.puts("cart service started")
{:ok, state}
end
@impl true
def handle_cast({:add, user, item}, carts) do
{:noreply, Map.update(carts, user, [item], &[item | &1])}
end
def handle_cast(:crash, _carts), do: raise("unexpected state!")
@impl true
def handle_call({:items, user}, _from, carts) do
{:reply, carts |> Map.get(user, []) |> Enum.reverse(), carts}
end
end
Logger.configure(level: :none) # keep the crash report out of this demo's output
{:ok, sup} = Supervisor.start_link([Cart], strategy: :one_for_one)
Cart.add("ana", "coffee")
Cart.add("ana", "mug")
Cart.add("ben", "tea")
IO.inspect(Cart.items("ana"), label: "ana")
IO.inspect(Cart.items("ben"), label: "ben")
old_pid = Process.whereis(Cart)
Cart.crash()
Process.sleep(50) # give the supervisor a moment to restart it
new_pid = Process.whereis(Cart)
IO.puts("restarted with a new pid: #{old_pid != new_pid}")
IO.inspect(Cart.items("ana"), label: "ana after restart (state was reset)")
Cart.add("ana", "beans")
IO.inspect(Cart.items("ana"), label: "ana")
IO.inspect(Supervisor.count_children(sup), label: "children")
Output
cart service started
ana: ["coffee", "mug"]
ben: ["tea"]
cart service started
restarted with a new pid: true
ana after restart (state was reset): []
ana: ["beans"]
children: %{active: 1, workers: 1, supervisors: 0, specs: 1}Run this on your computer — see “Get Started” for setup.
The restart resets the state: that's the point (a clean, known-good state), and also why important data belongs in a database, not only in process memory.
Common mistakes
- Doing slow work inside
handle_call, which blocks every caller of that GenServer; offload to Tasks. - One GenServer for everything, which becomes a bottleneck; partition state (one process per cart, a Registry, ETS tables).
castwhen you need confirmation. Usecallif the caller must know it succeeded.- Keeping critical data only in process state. Persist it; supervisors restart processes with fresh state.
- Unsupervised processes that silently disappear when they crash.
Key points
- GenServer = a stateful process with
init,handle_call(sync),handle_cast(async),handle_info. - Wrap calls in a client API; name processes to find them.
- Supervisors restart crashed children (
:one_for_oneand other strategies). - Persist important data; process state is a cache that can be reset.
Further reading: GenServer, Supervisor, Supervision trees and applications.
Practice questions
What's the difference between GenServer.call and GenServer.cast?
Answer
call is synchronous: the caller waits (with a default 5-second timeout) for handle_call's reply. cast is asynchronous: it sends the message and returns :ok immediately, handled by handle_cast without a reply.
What does a one_for_one supervisor do when a child crashes?
Answer
It restarts only the crashed child (with fresh state from init), leaving other children running. Other strategies like one_for_all and rest_for_one restart groups of children.
Open in the app to mark lessons complete, take notes and track your progress.