F5.06 · part 2 of 3
The engine GenServer
Three callbacks carry the whole engine. init/1 runs once on start: it folds
the event log into the starting state — the F5.05 reduce, run here. handle_call
for a command runs decide (the F5.04 contract, emitting events) and
evolve (folding them in), replies :ok, and keeps the new
state. handle_call for a query reads the state and replies with data,
leaving it unchanged. The command/query split from F5.05 becomes two clauses, and because a GenServer handles one
message at a time, writes serialize for free.
One process, three callbacks. Select one to see what runs and how the state threads through.
Three callbacks
Each callback takes the current state and returns the next one alongside a reply. init
builds the first state from the log; a command threads state in and a changed state out; a query threads state
in and the same state out. The single mailbox means these never overlap. Select a callback.
Engine callback · select one
callback: init/1
does: fold the log into state
A GenServer is the F5.05 reducer wearing a uniform: init folds, a command
evolves, a query reads — and the mailbox keeps them in line.
In code
The engine in three callbacks. init folds the log once with the F5.05
replay; the command clause runs decide and folds its events;
the query clause reads and replies. The contract from F5.04 lives inside decide, so a
bad command returns {:error, _} and leaves the state alone.
defmodule Portal.Engine do use GenServer # init folds the event log (F5.05) into the starting state, once @impl true def init(events), do: {:ok, replay(events)} # a command: run decide (contract + emit), evolve, keep the new state @impl true def handle_call({:enroll, user_id, course_id}, _from, state) do case decide(state, {:enroll, user_id, course_id}) do {:ok, events} -> {:reply, :ok, Enum.reduce(events, state, &evolve/2)} {:error, _} = err -> {:reply, err, state} end end # a query: read the state and reply with data; state is unchanged @impl true def handle_call({:courses_of, user_id}, _from, state), do: {:reply, State.enrollments_for(state, user_id), state} end
one message at a time
A GenServer's mailbox serializes calls, so writes never race — ordering is free.
split into clauses
The F5.05 command/query split becomes two handle_call clauses.
Next: supervision — what
restarts this process when it crashes, and how init replays the log to rebuild its
state.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Elixir — GenServer — a process that holds state behind a contract.
- Elixir — Agent — the minimal state-holding process.
- Erlang — ets — in-memory tables for shared, fast reads.