jonnifyelixir
F5 · Pragmatic Programming / F5.05 · cqrs

F5 · the engine · module 5

Commands, queries & events

The enroll command is contract-checked, but the engine still mixes two jobs — changing things and answering questions — and keeps no record of what changed. F5.05 separates them. A command changes state and returns only whether it worked; a query returns data and changes nothing; and every change is recorded as a past-tense event — %LearnerEnrolled{}, %LessonDelivered{}. Once changes are events, state stops being something you store and becomes something you derive: fold the events and you have the current state. That makes the engine a reducer — two pure functions, run by a fold.

Two paths through the engine and one record of change. Select a piece to see what it does and what it returns.

State is a left fold over the log

EVENT LOG · oldest to newest Enrolled Delivered Progressed fold next event in ACCUMULATOR · the engine state STATE %{} · nothing folded yet FOLDED 0 of 3 events

reduce(events, %{}, &evolve/2)
State starts empty. Each event folds in to derive the state at that point.

Writes, reads, and events

Reading the diagram: a command runs, emits an event, and the event folds into state — that is the write path. A query reads the same state and returns data without touching it — the read path. The event in the middle is what ties them together and what makes replay possible. Select a piece.

Through the engine · select a piece

WRITE PATH COMMAND enroll(...) EVENT %LearnerEnrolled{} STATE folded from events read READ PATH QUERY courses_of(user_id) returns data · no change

piece: Command

in the Portal: enroll(user_id, course_id)

Split writes from reads and record every change, and the engine gains a memory. State becomes a function of its history rather than a mutable pile you hope is correct.

Three deep dives

In order: the command/query rule and why it pays; modeling each change as a domain event; and folding those events so the engine becomes a reducer.

F5.05.1
Command/query separation
A function either changes state and returns success or failure, or returns data and changes nothing — never both.
F5.05.2
Domain events
Every change is a past-tense fact — %LearnerEnrolled{} — immutable and carrying the data of what happened.
F5.05.3
The engine as a reducer
A command emits events, each event evolves the state, and one reduce replays the engine to now.

F5.04 · honest commands

Enroll checks its contract, but it both changes state and is the only record that anything happened.

F5.05 · split and record

Writes and reads part ways, every change becomes an event, and state is the fold of the log.

Start with command/query separation, then domain events, then the engine as a reducer. The next module, F5.06 — Where engine state lives, chooses the process that holds the folded state. For the runtime path, see the design brief: the command & event flow.

References

Separating commands from queries, and modeling change as events.

Sources

Related in this course