Skip to content

Event Modeling Overview ​

Event Modeling is a way to design a system by laying its behaviour out on a timeline: what a user does, what command that becomes, what events it writes, and what read models fall out the other side. It is normally a whiteboard exercise. The whiteboard is then photographed, dropped in a wiki, and starts drifting away from the code the same afternoon.

JasperFx.Events carries the model as a semantic model in the codebase instead. Most of it is derived from code you have already written, so it cannot drift; the small remainder that code genuinely cannot express, you declare.

Derived from code, declared by you ​

Most of the model is stamped by the source that can already see it: Wolverine from its HTTP, handler and gRPC chains; the Bobcat generator from Gherkin specifications; a source generator from your projection types. That covers the command type, the handler, the aggregates, the events emitted, the messages published, the projections, the read models, the trigger kind and the slice pattern.

You can also declare the model yourself, which is how you do design-first work before any of that code exists: write stub types, declare the slices and their roles against them, and let the derived model take over as you build. See Declaring the Model in Code.

Declarations sit on the bottom rung of a three-rung ladder of authority -- declared below derived from code below observed in production -- so a hand-written line can never overwrite what the code actually does, and neither can overwrite what a running system was seen doing. When the code and your declaration disagree about a role, the code wins and the disagreement shows up as a hotspot. See provenance for how the ladder is applied, role by role.

Some things only ever come from declarations: a slice's name, its domain and chapter, the human label on its trigger ("Agent clicks Close"), and its specification links. Those are yours and stay yours.

The sample: IncidentService ​

Every example in this section models the IncidentService sample from the Wolverine repository — a small helpdesk where a customer logs an incident, an agent categorises it, and eventually somebody closes it.

Its events, aggregate and commands:

cs
public record IncidentLogged(Guid CustomerId, Contact Contact, string Description, Guid LoggedBy);

public record IncidentCategorised(Guid IncidentId, IncidentCategory Category, Guid CategorisedBy);

public record IncidentClosed(Guid ClosedBy);

snippet source | anchor

cs
public class Incident
{
    public Guid Id { get; set; }
    public int Version { get; set; }
    public IncidentStatus Status { get; set; } = IncidentStatus.Pending;
    public IncidentCategory? Category { get; set; }
    public bool HasOutstandingResponseToCustomer { get; set; }

    public void Apply(IncidentLogged _) { }
    public void Apply(IncidentCategorised e) => Category = e.Category;
    public void Apply(IncidentClosed _) => Status = IncidentStatus.Closed;

    public bool ShouldDelete(Archived @event) => true;
}

snippet source | anchor

cs
public record LogIncident(Guid CustomerId, Contact Contact, string Description, Guid LoggedBy);

public record CategoriseIncident(IncidentCategory Category, Guid CategorisedBy, int Version);

public record CloseIncident(Guid ClosedBy, int Version);

public record ArchiveIncident(Guid IncidentId);

snippet source | anchor

In the real sample each command has a Wolverine HTTP endpoint next to it — [WolverinePost("/api/incidents/close/{id}")] on CloseIncidentEndpoint, [Aggregate] on the Incident parameter. Those attributes are exactly what a derived source reads, which is why none of it is repeated in the overlay.

Slices and lanes ​

A slice is one vertical stripe of the timeline: one thing the system does, end to end. CloseIncident is a slice. So is GetIncident, and so is the archival that happens three days later.

Each slice renders across four lanes, top to bottom:

LaneHoldsExample in IncidentService
WireframeTriggers, external systems, hotspots"Agent clicks Close"
CommandThe command, the handler, the aggregatesCloseIncident → CloseIncidentEndpoint → Incident
EventStreamEmitted events and published messagesIncidentClosed, ArchiveIncident
ReadModelProjections and read modelsIncident

Every slice is also one of the four canonical Event Modeling patterns — Command, View, Automation or Translation — or a Query. The source derives the pattern from the shape of the code; you can also declare it before the code exists.

Query extends classic Event Modeling ​

Classic Event Modeling folds a read into the View slice: events, a projection, a read model, and a screen that shows it. The Critter Stack adds a fifth pattern, Query, and draws the read as its own slice:

PatternShape
Viewevents → projection → read model
Querytrigger → read of a read model, aggregate or document → response; changes no state

We separate them because an HTTP GET or a gRPC read in a Critter Stack system is real code. It has its own trigger, its own specifications and its own drilldown, and it might read an aggregate or a document instead of a projected view. Folded into a View, a query that reads Incident documents shows up as a projection "consuming" whatever events its specification arranged, and the canvas can't show both the projection and who reads it.

A Query slice names what it reads in ReadsFrom. When another slice produces that read model, the model links them with a ReadModelRead link, so the canvas draws projection → read model → query. A Query emits no events and sends no command; one that does is a hotspot.

What you actually write ​

The whole overlay for IncidentService is this:

cs
public class IncidentServiceModel : EventModelDefinition
{
    // Every source that contributes to "Helpdesk" is folded into one model under this name
    public override string Name => "Helpdesk";

    public override void Configure(EventModelBuilder builder)
    {
        builder.InDomain("Incidents");

        builder.Slice("LogIncident")
            .TriggeredBy("Customer submits the incident form")
            .LinksToSpecification("Log Incident/Logs an incident from the web form");

        builder.Slice("CategoriseIncident")
            .TriggeredBy("Agent picks a category");

        builder.Slice("CloseIncident")
            .TriggeredBy("Agent clicks Close");

        builder.Slice("ArchiveIncident")
            .InDomain("Retention")
            .TriggeredBy("Three days after close");

        builder.Slice("GetIncident")
            .TriggeredBy("Agent opens the incident detail screen");
    }
}

snippet source | anchor

Five slice names, four trigger labels, one domain, one specification link. Nothing about CloseIncident the command, CloseIncidentEndpoint the handler, or IncidentClosed the event — Wolverine already knows all three, and repeating them here would only give them somewhere to drift to.

Where to go next ​

  • The Overlay — the authoring API in full, plus registration and the escape hatch for flows you do not own.
  • Hotspots — recording what the model has not decided yet.
  • Descriptors — the wire shape, the rendering contract, and how sources are assembled into one model.

Released under the MIT License.