F5.05 · part 2 of 3
Domain events
An event is a record that something happened, written in the past tense:
%LearnerEnrolled{}, %LessonDelivered{},
%ProgressRecorded{}. It is a plain, immutable struct that carries the data of the change
and the time it occurred — not a command to do something, but the fact that it was done. Where a command
can fail, an event already happened and cannot be argued with. Naming changes this way gives the engine a
vocabulary for its own history, and that history is exactly what the next dive folds back into state. Events
carry their own branded id under an EVT namespace, so each is timestamped for free.
Three changes the Portal records. Select an event to see what happened and what it carries.
Facts, in the past tense
The tense is deliberate. A command is an imperative — enroll this learner — and might be refused. An event is a report — this learner was enrolled — and is settled. Because an event is immutable and self-describing, you can store it, replay it, or hand it to another part of the system without fear. Select an event.
Portal events · select one
event: LearnerEnrolled
records: a learner joined a course
A command might not happen; an event already did. That difference is why events, not commands, are the thing worth keeping.
In code
An event is a struct like any domain type — enforced keys, a typespec — named in the past tense.
The command builds one on success, stamping it with an EVT id whose snowflake gives
the time for free.
defmodule Portal.Events.LearnerEnrolled do @enforce_keys [:id, :user_id, :course_id, :at] defstruct [:id, :user_id, :course_id, :at] @type t :: %__MODULE__{ id: String.t(), user_id: String.t(), course_id: String.t(), at: DateTime.t() } end # enroll emits it on success — a past-tense fact, stamped with an EVT id and its time defp enrolled_event(user_id, course_id) do id = Portal.ID.new("EVT") %LearnerEnrolled{id: id, user_id: user_id, course_id: course_id, at: Portal.ID.at(id)} end
name the change
Past tense, immutable, self-describing — a fact you can store and replay.
stamped in time
Each event carries an EVT id whose snowflake decodes to at.
Next: the engine as a reducer — folding these events into the current 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.