jonnifyelixir
Contents / F5 · Pragmatic Programming / The command & event flow

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

command EnrollLearner contract preconditions event LearnerEnrolled state apply event query read projection write path · only events change state read path

      

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

Related in this course