# Event Storming Modeling

> Reference for modeling a domain with Event Storming and tactical/strategic DDD: the sticky-note color vocabulary, the command-aggregate-event-policy flow grammar, and the conflations to avoid. Use when building or reviewing an event-storming diagram or model, choosing colors/labels for domain concepts, deciding whether something is a Policy vs an Invariant, representing aggregates/commands/events/read models/external systems/actors, mapping a DDD domain, generating such diagrams from code, or recovering a model from annotated .NET assemblies with Hindstorm. Covers what a topological (code-derived) view loses versus a workshop wall, and when a domain is a streaming/dataflow transformation pipeline (where forcing the command/aggregate grammar is over-modeling). For those, names the second-plane vocabulary: CEP / event abstraction hierarchy, soft real-time, supporting vs core subdomain, data on the inside vs outside, domain event vs measurement, and Event Modeling's Automation and Translation patterns, joined to

- Skill: `blakehastings/event-storming-modeling` (Agent Skill)
- Install (CLI): `npx skillmds@latest add blakehastings/event-storming-modeling`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blakehastings/event-storming-modeling/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: BlakeHastings (https://skillmd.com/u/blakehastings)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blakehastings/event-storming-modeling

---


# Event Storming & DDD Modeling Conventions

Cross-checked against Brandolini's *Introducing EventStorming*, the "Picture That Explains Everything,"
and practitioner sources. Event Storming has no rigid spec — these are the de-facto community standards.

## Sticky colors (the vocabulary)

| Concept | Color | Meaning |
|---|---|---|
| **Domain Event** | orange | Something that happened, past tense ("OrderPlaced"). The most stable convention. |
| **Command** | blue | Intent to change state ("PlaceOrder"). |
| **Actor / User** | small yellow | A person/role that issues commands. Distinct by size/shape from the aggregate. |
| **Aggregate** | large/pale yellow | Receives commands, enforces invariants, emits events. |
| **Policy** (process/reaction) | lilac / purple | "Whenever \<event\> then \<command\>" — reactive automation or human procedure. |
| **Read Model / View** | green | The data an actor reads before deciding. |
| **External System** | pink | Outside the boundary; issues commands or receives/raises events. |
| **Hotspot** | red | Friction, disagreement, unknowns. Human judgment — cannot come from code. |

Caveats: **Actor color is the softest convention** (often just a small yellow with a stick figure; treat
actor vs aggregate as different shapes/sizes of yellow). Pink is sometimes also used for bounded contexts.
Don't hard-assert one hex per color across legends. In a flat digital diagram you lose the size/shape cue,
so give actor and aggregate **genuinely different hues** or they're indistinguishable.

## The flow grammar ("The Picture That Explains Everything")

The canonical, self-reinforcing loop:

```
Actor (or Policy, or External System)  --issues-->  Command
Command  --invoked on / handled by-->  Aggregate
Aggregate  --emits / raises-->  Domain Event
Domain Event  --triggers / reacts to-->  Policy
Policy  --issues-->  Command            (loop)
Domain Event  --updates-->  Read Model  --read by-->  Actor
External System  --raises-->  Domain Event  (and can receive commands)
```

Hard rule worth encoding: **aggregates only receive commands and emit events — they never issue commands.**
Commands come from actors, policies, or external systems. Common edge verbs: *issues*, *invoked on* /
*handles*, *emits* / *raises* / *produces*, *triggers* / *reacts to*, *updates*, *enforces* (an invariant).

## Policy vs Invariant vs Business Rule (the conflation to avoid)

These are **different concepts** — labeling all of them "Policy" is a real modeling error:

- **Policy** (lilac) is strictly **reactive**: "whenever \<event\> then \<command\>." It sits *after* an
  event and *before* the next command, orchestrating cross-aggregate reactions. Defined by being
  triggered *by an event*.
- **Invariant / Business Rule** is a constraint enforced *inside* an aggregate while it handles a command,
  *before* any event ("reject order if stock unavailable," "cannot refund more than paid"). It governs
  whether a command is allowed to succeed; it is **not** event-triggered and does **not** issue a command.
  Classic Event Storming keeps it inside the aggregate or on a separate "business rule" sticky — never as
  a lilac Policy. Related DDD terms: invariant, specification, guard.

So: policies are *reactive logic triggered by events*; invariants are *constraints maintained within an
aggregate's transaction boundary*. Model them as distinct things with distinct labels/colors.

## Streaming / dataflow contexts: the second plane

The command/aggregate/event/policy grammar is built for **transactional** domains: an actor issues intent,
an aggregate guards invariants, and a decision can succeed or be **rejected**. A **streaming / dataflow**
context (audio to segment to text to decision; ETL; sensor and inference pipelines) is a different plane.
It continuously **transforms** immutable records that already happened and **cannot reject** anything.
Do not force one grammar onto both. Model two planes joined at a named seam:

```
sensor -> transform -> transform -> [ TRANSLATION / DECISION ] -> domain event -> policy -> command -> aggregate
|------------- dataflow plane -------------|       seam (ACL)      |-------------- domain plane --------------|
   continuous, immutable, can't reject                              discrete, has intent, can reject
```

Everything left of the seam is plumbing; only domain-significant facts cross it. It is the same seam under
many names: an Anti-Corruption Layer (Evans), Event Modeling's Translation/Automation slice (Dymitruk),
CEP's abstraction step (Luckham), the IoT "threshold-to-event," the ML "score-to-label."

**Vocabulary to reach for (name the plane instead of forcing stickies onto it):**

- **Complex Event Processing (CEP) / event abstraction hierarchy** (Luckham): the formal name for a pipeline
  that lifts a high-frequency stream up levels of meaning (frame -> segment -> utterance -> intent), deriving
  a smaller number of significant events from many low-level ones. Transform stages are *Event Processing
  Agents*, not aggregates.
- **Soft real-time** (vs hard/firm): a late result degrades quality but does not crash or corrupt. This is
  the license to keep aggregates, transactions, and ceremony out of the hot path; tactical DDD in the
  latency path is a known mismatch.
- **Supporting / generic subdomain** vs **core domain** (Evans): the pipeline is usually supporting/generic
  plumbing; the decisions and what consumes them are core. Different subdomains deserve different modeling
  effort by design, not by accident.
- **Domain event vs measurement / telemetry / data-point**: the discriminator for what crosses the seam. A
  raw frame or score is a measurement; a named business fact ("UtteranceTranscribed", "ThresholdExceeded")
  is a domain event. "A row was inserted" is not a domain event; "a delivery was cancelled" is.
- **Data on the inside vs data on the outside** (Helland): inside data is rich, mutable, in-process; outside
  data is immutable, slim, versioned, on the wire. A rich in-process domain event and the slim integration
  event published from it are not duplication, they are the same fact in two registers.
- **Event Modeling's Automation and Translation patterns** (Dymitruk): the grammar Event Storming lacks for
  transforms. A pure transform that reacts to data with no human and no decision is an **Automation** slice,
  not a Policy. The same step crossing a system boundary is a **Translation** slice.

**Decision rule (transform stage vs decision point):**

- A stage is **dataflow infrastructure** (a filter / processor / EPA, leave it off the domain wall) when it
  always runs, validates nothing, rejects nothing, and just produces the next representation. "Do the next
  step" is a pipeline edge, not a command, and "proceed to the next stage" is not a Policy.
- A stage is a **genuine command / decision** only when intent can be **validated, rejected, or routed**
  (e.g. "is this relevant?", "dispatch to the agent"), or when the same step is exposed across a **service
  boundary** as a request: an in-process function call is ceremony, but a `TranscribeUtterance` message sent
  to a remote worker **is** a real command. The same operation can be ceremony in-process and a legitimate
  command across a boundary.
- There are **no aggregates** in the pure pipeline: nothing holds a consistency boundary with invariants over
  entities. Stateful processors are **domain services**, not aggregates; do not tag them Aggregate by reflex.
- A node that exchanges no domain event does not belong on the wall: a VAD model handing a probability to an
  endpointer is part of that step, not a separate external system. Drop it rather than leave it floating.

Sources for this section: Helland, "Data on the Outside vs. Data on the Inside" (CIDR 2005); Luckham, *The
Power of Events* and the CEP literature; Dymitruk, eventmodeling.org (Automation/Translation); Kleppmann,
*DDIA* ch. 11 and "Event Sourcing & Stream Processing at DDD Europe"; Fowler, "What do you mean by
Event-Driven?"; de la Torre (Microsoft), "Domain Events vs Integration Events." Re-verify exact wording with
WebSearch when it must be precise.

## Tactical building blocks are mostly implied

**Value Objects and Entities do not belong on an Event Storming wall.** The wall is behavioral
(commands/events/policies/aggregates/read models/actors/external systems); tactical structure is implied,
not drawn. Modeling a Value Object (e.g. `Money`) as its own node is an extension beyond the method — fine
if deliberate, but flag it as tactical and expect it to sit apart from the flow.

## Strategic: bounded context is declared, not inferred

A bounded context is a strategic boundary. When tooling groups concepts by context, that context must be
**declared** (an explicit label), never guessed from namespace/folder structure — code layout is an
accident, not a domain fact (the same reason you don't infer behavior from method bodies). Draw each
context as a labeled boundary box; edges that cross a boundary are the integration points (ACL /
published language).

## What a code-derived (topological) view loses

A graph extracted from code captures nodes and edges but not the narrative/spatial semantics of a wall:

- **Timeline order** — events read left-to-right in the order they happen; a topological graph is not a
  timeline (horizontal position = reachability, not chronology).
- **Pivotal events** — major state transitions that divide phases/contexts; not derivable.
- **Bounded-context grouping** — emerges where the ubiquitous language changes; needs a declared boundary.
- **Swimlanes** — parallel/alternative streams; a flat graph collapses concurrency.
- **Hotspots** — friction/unknowns; human artifacts, absent from any code-derived view.

Treat a code-derived storm as an always-true scaffold and conversation starter, not a replacement for a
workshop.

## Recovering a model from code with Hindstorm

Hindstorm (nuget.org/packages/Hindstorm) recovers an Event Storming model from .NET assemblies via
attributes (`Hindstorm.Annotations`) and exports Mermaid/DOT/JSON (the `Hindstorm` package). Observations
from use (scope to the package version you have; these are from the 0.2.0 era, verify before relying):

- **Concept tags and relation attributes have different valid targets.** Concept tags (`[Aggregate]`,
  `[DomainEvent]`, `[ExternalSystem]`, `[Invariant]`, ...) allow class/struct, and `[ExternalSystem]` also
  allows interface. **Relation attributes (`[Raises]`, `[ReactsTo]`, `[Handles]`, `[Issues]`, `[Enforces]`,
  `[Updates]`) are valid only on class/struct/method, not interfaces** (you get CS0592). To state an edge
  from an interface-typed external system, put the relation on the **method** that performs it (e.g.
  `[Raises]`/`[ReactsTo]` on `TranscribeAsync`); the scanner reads method-level relations on any tagged
  concept, and it is more precise anyway.
- **The scanner uses live reflection** (`GetCustomAttribute`, `GetMethods`), so the assemblies must be
  **loaded** in the generating process with their dependencies resolvable. `Scan(IEnumerable<Assembly>)`
  takes many assemblies, so one pass yields a unified model.
- **An Aspire AppHost cannot scan the app assemblies**: Aspire project references are
  `ReferenceOutputAssembly=false`, so the app DLLs are not in the AppHost output and its types are not
  loadable there. Centralize generation in a **dedicated tool project** with real `ProjectReference`s to
  the annotated projects (deps resolve via its `deps.json`) that discovers assemblies from its own output
  directory, so onboarding a new service is one project reference and no code change. (A future
  metadata-only scan via `System.Reflection.MetadataLoadContext` would remove the loaded-deps requirement
  but needs Hindstorm to read `CustomAttributeData` instead of instantiating attributes.)
- Frame the artifact: emit a `.md` embedding the Mermaid in a fenced block (renders on GitHub; a raw
  `.mmd` does not) with a one-line note on the context's nature so readers judge it on its own terms.

Hindstorm deliberately stays **Event-Storming-pure**: the dataflow plane (transform processors, measurement
data events, the abstraction hierarchy, translation/ACL seams; see "Streaming / dataflow contexts" above) is
NOT part of its vocabulary, a scope decision made 2026-06-22. To model a streaming pipeline with the existing
stickies, represent transform stages as **external systems** or **domain services**, leave raw frames/scores
off the wall as plumbing, and promote only the abstraction-top facts to `[DomainEvent]`. The two-plane model
is a thinking tool, not a Hindstorm annotation set.

## Sources

Brandolini, *Introducing EventStorming* / "The Picture That Explains Everything"; wwerner
event-storming-cheatsheet; Open Group O-AA; IBM refarch-eda; Qlerify; Context Mapper; Avanscoperta;
plus practitioner blogs on design-level Event Storming. Re-verify specifics with WebSearch when a claim
must be exact — colors and grammar are conventions, not a standard.

