Skip to content

Event Modeling

The Cratis Stack

Design the flow before you write the code

The hard part of an event-sourced system isn’t the plumbing — it’s deciding what the events are, where commands come from, and which read model each screen needs. Get that wrong and you find out late, in code. Event modeling is a way to lay the whole flow out on a single timeline first. And because that timeline’s pieces are exactly Cratis’s building blocks, the model you draw is the blueprint for your slices.

Event modeling describes a system as a timeline of information moving between people and machines — you read it left to right, like a comic strip. A user does something on a screen, that issues a command, the command records an event, and events are projected into read models that feed the next screen. Nothing else. The whole notation is five kinds of sticky note:

UI/A: ReservationsUI/A: ReservationsUI/A: ReservationsC/RM: ReservationsC/RM: ReservationsC/RM: InventoryStream: ReservationsStream: Inventory
ReserveBookScreen
ReserveBook
BookReserved
Availability
CatalogScreen
StockKeeper
DecreaseStock
StockDecreased

Read it as a story. A reader reserves a book on a screen (ui); that fires the ReserveBook command (cmd), which records the BookReserved event (evt) — an immutable fact. That fact is projected into an Availability read model (rmo) that the catalog screen reads. And a processor (pcr) watching reservations quietly issues a follow-up command to decrease stock. The rows are swimlanes — one per bounded context (Reservations, Inventory).

That’s the entire vocabulary: wireframe, command, event, read model, processor. No class diagrams, no database schemas — just the flow of facts.

A timeline this plain does three things a box-and-arrows architecture diagram can’t:

  • It’s a shared language. A domain expert and an engineer read the same picture. The expert can point at BookReserved and say “no, that only happens after payment” — a correction that would otherwise surface as a bug three sprints later.
  • It forces completeness. Every event must trace back to a command; every command to a screen or a processor; every screen must read a real read model. Walk the timeline and the gaps light up — a screen with no data behind it, an event nothing produces. You find the holes on a whiteboard, not in production.
  • There are only four shapes. Every connection in a correct model is one of four repeating patterns. Once you can see them, you can see any feature as a handful of small, known pieces.

These four shapes are the whole grammar. Every slice of every system is built from them:

PatternShapeWhat it does
Commandui ➜ cmd ➜ evtA user acts; a command validates and records a fact. The write side.
Viewevt ➜ rmo ➜ uiFacts are projected into the state a screen reads. The read side.
Automationevt ➜ pcr ➜ cmdA processor reacts to a fact and issues a command on its own.
Translationexternal evt ➜ pcr ➜ cmdA fact from another context is adapted into this one’s commands.

The first two are the everyday CQRS loop. The last two are how work flows between contexts without anything being directly coupled — a processor watches for a fact and acts. A translation begins on a reset frame (rf) because its triggering event comes from somewhere else:

UI/A: OrdersC/RM: OrdersStream: PaymentsStream: Orders
PaymentReceived
PaymentTranslator
ConfirmOrder
OrderConfirmed

Here’s the part that makes event modeling more than a whiteboard exercise for Cratis: every block is a real Cratis primitive, and the four patterns are literally the four kinds of vertical slice Cratis already organizes code around. The model isn’t a sketch you translate — it’s the slice, drawn.

Event-model blockWhat it meansIn Cratis
Wireframe (ui)a screen the user seesa React screen — Components + Arc.React
Command (cmd)an intent to change statea [Command] record with Handle()Arc
Event (evt)a fact that happenedan [EventType] record in Chronicle
Read model (rmo)state a screen readsa [ReadModel] built by a projection
Processor (pcr)automation that reacts to factsan IReactor in Chronicle
Swimlanea bounded contexta vertical slice / feature folder

And the patterns line up exactly with how a Cratis slice is classified:

Event-modeling patternCratis slice type
CommandState Change — command + events
ViewState View — read model + projection
AutomationAutomation — a reactor that decides and acts
TranslationTranslation — a reactor that adapts events across slices

This is not a coincidence — both come from the same idea, that events are the source of truth and everything else is derived from them. A [Command]’s Handle() returns the event it records; a [ReadModel] declares the events it’s built from; an IReactor reacts to a fact and issues the next command. Draw the timeline and you’ve named your commands, your events, your read models, and your reactors — in the order they happen.

Event modeling already has the given/when/then shape baked in. The left side of the column is the context, the command is the behavior under test, and the event/read-model blocks are the expected outcome:

Event-model blockSpecification role
Existing events before the commandGiven
Command or reactor in the columnWhen
New event, projected read model, or side effectThen

That is why Cratis uses BDD-style specifications so heavily. Cratis.Specifications.XUnit gives xUnit the Establish() / Because() / [Fact] lifecycle, while Arc and Chronicle add scenarios that speak the same language: CommandScenario<TCommand> for the command, EventScenario.Given for past facts, ReadModelScenario<TReadModel> for projections, and ReactorScenario<TReactor> for automation.

Testing with Cratis shows a concrete event-model column translated into an executable stack spec.

Event modeling earns its keep when behavior is interesting — when facts accumulate, screens derive state from history, and work flows between contexts. It’s overkill when there isn’t much of a story to tell: a settings form that reads and writes a single row, a static lookup table, a pure request/response with no meaningful event worth keeping. If a feature is honestly just CRUD over one record, model it as CRUD. Reach for event modeling when the flow — not the storage — is where the complexity lives.