F5.05 · part 1 of 3
Command/query separation
Command/query separation is one rule, due to Bertrand Meyer: a function either changes
state and returns whether it worked, or returns data and changes nothing — never
both. A command does work and reports success or failure; a query answers a question and leaves the world
untouched. enroll is a command; courses_of is a query. The
payoff is reasoning: a query can be called twice, cached, or run in parallel because it has no effect, and a
command is honest about the fact that it changes things. It also nudges enroll away
from returning a read model — on success it reports :ok, and the caller queries
for the result.
Two kinds of function, one job each. Select one to see what it returns and what it touches.
Change or read, not both
The trouble with a function that both changes state and returns data is that you can never call it safely only to look — every read risks a write. Separating the two means a query is a free, repeatable observation, and a command is the only thing that can move the engine. Select a kind.
Command or query · select one
kind: Command
returns: :ok | {:error, _}
If a function returns data, it must be safe to call again. The only way to promise that is to keep it from changing anything — which is the whole rule.
In code
Two functions, two shapes. enroll changes state and returns only a tag;
courses_of returns data and runs no effect. After a successful command, the route
renders by issuing a query — the write path and the read path stay separate.
# command — changes state; returns success or failure, not a read model @spec enroll(String.t(), String.t()) :: :ok | {:error, atom} def enroll(user_id, course_id) do with :ok <- check_precondition(user_id, course_id) do emit(%LearnerEnrolled{user_id: user_id, course_id: course_id}) # record the change end end # query — returns data; changes nothing, safe to repeat or run in parallel @spec courses_of(String.t()) :: [Enrollment.t()] def courses_of(user_id), do: State.enrollments_for(user_id)
ask or act
A query observes for free; a command is the only thing that moves the engine.
enroll, then query
The command reports :ok; the caller reads back the result.
Next: domain events — the past-tense facts a command emits when it changes state.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Martin Fowler — CQRS — separate the write model from the read model.
- Martin Fowler — Event Sourcing — state as a log of events to fold over.
- Commanded — CQRS/ES building blocks in Elixir.