F5 · system design · 3 of 3
The command & event flow
The structure and the data are set; the last piece of the design is how the engine moves. Every change follows the same path: a command states an intent, a contract checks it, a domain event records what happened, the event transitions state, and a separate query reads a projection. Writes and reads never share a door — commands change the world and return events, queries read the world and change nothing — so the surface the UI calls stays small and predictable. This is the flow F5.04, F5.05, and F5.06 build.
Follow one use case — enrolling a learner. Select a stage to see what it does and which module builds it.
One use case, five stages
The write path is the first four stages, left to right; the read path is the fifth, on its own. Keeping them apart is the whole trick: because only events change state, the engine has one auditable list of what ever happened, and because queries are pure, the UI can read freely without risk. Enrolling a learner walks the whole path once.
The flow · select a stage
stage: command
built by: F5.05
One path for every change, and a separate one for every read. Decide the flow once, here, and each command the engine grows later slots into the same five stages.
The flow in code
The write path is a with chain that fails fast: the contract first, then the event,
then the state transition, each step short-circuiting to a tagged error if it cannot proceed. The read path is
a separate function that never touches a command. The two functions are exactly the
dispatch/1 and query/2 from the blueprint — this is what
is behind that edge.
# write: command -> contract (F5.04) -> event (F5.05) -> state (F5.06) def dispatch(%EnrollLearner{} = cmd, state) do with :ok <- check(cmd, state), # contract — or {:error, reason} event = build_event(cmd), # %LearnerEnrolled{...} next_state = apply_event(state, event) do # pure reducer {:ok, event, next_state} end end # read: a pure projection — no command, no side effects (F5.05) def query(:courses_of, user_id, state), do: Map.get(state.enrollments, user_id, [])
a command
An intent the UI submits — EnrollLearner.
an event, then new state
Checked, recorded as LearnerEnrolled, applied by a pure reducer.
That completes the design brief — structure, data, and flow. Back to the chapter overview to start F5.01, or revisit the blueprint and the domain model.
References
Separating writes from reads, and modeling change as events.
Sources
- Martin Fowler — CQRS — the command/query split.
- Martin Fowler — Domain Event — change as a past-tense fact.
- Elixir —
with— the happy-path chain that fails fast.
Related in this course
- F5.0.1 · The Portal engine blueprint — where dispatch and query live.
- F5.0.2 · The domain model — the entities the events change.
- F4.09 · Branded CHAMP maps & GenServer — where engine state is kept.
- F5 · Pragmatic Programming