jonnifyelixir
F5 / F5.09 / end-to-end

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

APPLICATION · supervisor start_link([store, engine], strategy: :one_for_one) supervises ENGINE · GenServer init reads + replays · handle_call decides + appends holds state EVENTSTORE · the port append/2 · read_stream/1 — the adapter holds the log durable

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

APP.START Supervisor STORE READY adapter started ENGINE.INIT read_stream + replay READY {:ok, state} a restart re-runs init; the same log folds to the same state — recovery is replay

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

Related in this course