F5.09 · part 1 of 3
The engine facade end to end
Assembly is where the contracts pay off. The Portal.Application starts two
children under one supervisor: the configured event-store adapter (F5.08.1) and the engine (F5.06). When the
engine starts, its init/1 reads the whole stream through the port and folds it with
replay/1 (F5.05) into the starting state. A command then runs the full path:
Portal.enroll/2 calls the engine, which runs decide (the F5.04
contract), appends the events through the port so they are durable, folds them with
evolve, and replies. Because the log is the source of truth, a crash and restart replays
it back to the same state. Nothing here is new code; it is eight modules wired into one running system.
One supervised system, three parts. Select a part to see its role at runtime.
The running system
Three things run together. The Application is the supervisor; the engine is
the GenServer that holds the folded state and answers calls; the store is whichever adapter
the port resolved to. The engine talks to the store only through the port, and the facade talks to the engine
only through command/1 and query/1. Select a part.
The running system · select a part
part: Application
its role: starts the store and the engine, supervised
The runtime is three named parts with one rule between each pair: the engine reaches the store only through the port, and the facade reaches the engine only through two wrappers. Wire those rules once and the system has no back doors.
The supervision tree
The Application is a handful of lines: start the configured store and the engine under one supervisor with a
:one_for_one strategy, so if either crashes it is restarted on its own. The store
comes first because the engine reads from it during init.
defmodule Portal.Application do use Application @impl true def start(_type, _args) do children = [ Portal.EventStore.adapter(), # the configured store process (Agent, or Ecto.Repo) {Portal.Engine, []} # the engine; its init reads the log and replays ] # one_for_one: a crashed child is restarted on its own (F5.06) Supervisor.start_link(children, strategy: :one_for_one, name: Portal.Supervisor) end end
Boot: read, replay, ready
The engine's init/1 is where F5.05 and F5.08 meet: it reads the whole stream through
the port and folds it with replay/1 into the starting state. A command clause then
decides, appends the new events through the port, and evolves the in-memory state, so the log and the state
never disagree.
defmodule Portal.Engine do use GenServer alias Portal.{EventStore, Error} def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) # init reads the whole stream through the port and folds it into state @impl true def init(_) do {:ok, events} = EventStore.read_stream("portal") {:ok, replay(events)} end # a command: decide, persist through the port, then evolve in memory @impl true def handle_call({:command, cmd}, _from, state) do case decide(state, cmd) do {:ok, events} -> :ok = EventStore.append("portal", events) {:reply, :ok, Enum.reduce(events, state, &evolve/2)} {:error, _} = err -> {:reply, err, state} end end @impl true def handle_call({:query, q}, _from, state), do: {:reply, answer(state, q), state} end
Boot sequence
A command, persisted
With the tree up, a command is one synchronous call that ends in a durable event. The append happens before
the in-memory fold, so a crash after the append still replays the change; a crash before it loses nothing that
was acknowledged. Verifying boot, a command, and a restart from iex:
# start the app, run a command, confirm a restart replays the log iex> Portal.enroll("USR0KH...", "CRS0KH...") :ok iex> Portal.progress_of(enrollment_id) {:ok, 0} # crash the engine; the supervisor restarts it and init replays the appended events iex> Process.exit(Process.whereis(Portal.Engine), :kill) iex> Portal.progress_of(enrollment_id) {:ok, 0} # state is back: the log, not the process, was the source of truth
wire, do not rewrite
The Application only connects parts each earlier module already finished and tested.
durable by replay
Append before evolve, and a restart re-reads the log to the same state.
Next: a LiveView mount sketch — the web layer that calls this facade and renders from its results.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Phoenix — Phoenix.LiveView — server-rendered, stateful UI over the engine.
- Phoenix — Phoenix.Component (HEEx) — function components and the HEEx template.
- Elixir — Supervisor — the engine runs under a supervision tree.