Discovering the Domain with EventStorming: From Big Picture to Aggregates
EventStorming is a workshop format that brings domain experts and developers to a shared language by placing what happens in the domain on a timeline as orange, past-tense stickies. We take Bookstore's order flow through the Big Picture, Process Modeling and Software Design levels and turn the hot spots, pivotal events, policies and aggregate candidates from the wall into code.
Version note: The code in this post was built and run with C# 14 and .NET 10.
Guess First
The Bookstore team spends an afternoon in a meeting room. Next to the three developers sit Grace from customer service, Linus from the warehouse and Ada from finance. An eight-metre paper roll hangs on the wall, and everyone holds a pad of orange sticky notes. There is one rule: "write something that happens in the order process, in the past tense". Twenty minutes later these seven notes are on the wall, in no particular order:
PaymentRefunded OrderShipped OrderPlaced ReturnRequested
StockReserved OrderDelivered PaymentCaptured
Two questions. In what order would you put these events on the timeline? And while ordering them, which single point are two people in the room guaranteed to argue about? A hint: Ada thinks the money should be taken first, Linus thinks you should never charge for a book that isn't on the shelf.
What Is EventStorming?
EventStorming is a workshop format that Alberto Brandolini introduced in 2013 in a blog post titled "Introducing Event Storming". The idea is simple: to understand a business process, you don't start with the data model, the screens or the tables, you start with the things that happen in the domain. Each "thing that happened" is written in the past tense on an orange sticky (a domain event) and placed on a timeline that flows from left to right. Brandolini has since been collecting the method in a book called "Introducing EventStorming", published on Leanpub.
Why start with events? Because events are the domain expert's language. Linus doesn't say "the Reserved column in the stock table goes up", he says "the book was reserved". Ada doesn't say "a row is inserted into Payments", she says "the payment was captured". The past tense is deliberate too: "capture payment" is an intention, "payment captured" is a fact. Intentions get debated, facts get ordered. The raw material of the ubiquitous language we discussed in the second post of this series shows up on this wall.
The second important idea is unlimited modeling space. A whiteboard fills up and a slide scrolls away, but a paper roll just gets longer. The room matters as well: chairs are pushed aside, everyone stands in front of the wall. The DDD (Domain-Driven Design) literature keeps saying "work with the domain expert", and teams rarely do; EventStorming turns that advice into a concrete, afternoon-long activity.
Three Levels: Big Picture, Process Modeling, Software Design
The same notation is used for three different purposes. DDD Crew's open source "EventStorming Glossary & Cheat Sheet" separates the three formats like this:
| Level | Question | Who attends | Output |
|---|---|---|---|
| Big Picture | How does this business work as a whole, and where does it hurt? | 10–30 people: every department, developers | Timeline, hot spots, pivotal events, bounded context candidates |
| Process Modeling | How does this process run step by step, and who decides what based on which information? | The process owners and the team that will build it | A consistent flow of commands, policies, read models and actors |
| Software Design | With which design and which rules will we turn this flow into code? | The bounded context's team and the domain expert | Aggregate (constraint) candidates, command/event contracts |
The order matters. In the Big Picture nobody asks "which class does this go in"; the moment someone does, the meeting turns into a developers' meeting and Grace goes quiet. In Process Modeling you clarify the flow. In Software Design you are inside a single bounded context and you talk about where each rule is protected. In this post we take Bookstore's order flow through all three levels.
The Color Legend
EventStorming's strength is how little notation it has. Everyone learns it in five minutes, and nobody needs to know UML (Unified Modeling Language). The legend we used in the Bookstore session:
- Orange, domain event:
OrderPlaced,StockReserved. Past tense, in the domain's language. - Blue, command:
PlaceOrder,ReserveStock. The intention of an actor or a policy; it sits right to the left of its event. - Small yellow, actor: the customer, the warehouse clerk. Who issues the command?
- Lilac, policy: "when
OrderPlaced, reserve stock". A "whenever X happens, do Y" rule that links an event to a command; sometimes a person, sometimes automation. - Green, read model: the "My orders" screen, the warehouse picking list. The information an actor looks at to make a decision.
- Wide pink, external system: the payment provider, the carrier's API (Application Programming Interface).
- Large pale yellow, aggregate: the thing that protects the rules and accepts or rejects a command. DDD Crew now calls this note a "constraint"; saying "aggregate" to a domain expert in a Big Picture session confuses people, asking "which rule lives here" doesn't.
- Red or neon pink, hot spot: a question, a conflict, a pain point. You don't solve it, you mark it.
The Bookstore Session: Big Picture
The session follows the steps Brandolini describes. The first twenty minutes are "chaotic exploration": everyone silently writes the events they know and sticks them on the wall. The same thing gets written under three different names ("order received", "order created", "checkout completed"); that isn't a problem, it is the start of the ubiquitous language conversation. Then the timeline gets enforced: events move into left-to-right order, duplicates merge, names get agreed. The team settles on OrderPlaced, the name in the series' glossary.
This is where the opening question gets its answer. The happy path:
OrderPlaced → StockReserved → PaymentCaptured → OrderShipped → OrderDelivered
→ ReturnRequested → PaymentRefunded
The argument is between StockReserved and PaymentCaptured. Ada wants to capture the money immediately, because the payment provider slows down during campaigns. Linus tells the story of last year's year-end campaign, when customers were charged for books that weren't in stock and refunds took two weeks. Nobody is right or wrong; this is a hot spot. The facilitator doesn't try to resolve it, they stick a red note on the wall and move on. The other hot spots that pile up and get discussed: "What happens to the order if there isn't enough stock?", "What if the payment provider doesn't answer within 5 seconds?" and "Who pays for return shipping?".

Next come the pivotal events: the few critical events where the process moves from one phase to the next and ownership changes hands. In Bookstore these are OrderPlaced (from the order team to the warehouse), PaymentCaptured (from finance to fulfilment), OrderShipped (from us to the carrier) and ReturnRequested. Draw vertical lines between these events and the wall splits into regions by itself, each with its own language and its own owner. The Ordering, Inventory, Payments and Shipping bounded contexts we discussed in posts 04 and 05 of this series sit on top of those regions. The Big Picture doesn't "draw" bounded contexts; it shows candidates. The final boundaries are set when the context map is discussed, and in the code.
Process Modeling: Policies and Read Models
The next day a smaller group focuses only on the stretch from "order placed" to "order shipped". At this level, to the left of each event sits the answer to "what triggered this?": a command issued by an actor, or a policy reacting to an earlier event. Brandolini reads this sequence like a grammar: an actor looks at a read model and issues a command; the command is handled by a system or an aggregate; the outcome is an event; the event triggers a policy, and the policy produces a new command.
Two important things become clear for Bookstore at this level. First, most policies in the flow are automation, not people: "when OrderPlaced, reserve stock" and "when StockReserved, capture payment" don't wait for anyone's approval. Second, yesterday's hot spot gets its answer as a policy: "when OutOfStock, cancel the order and notify the customer". The argument between Ada and Linus turns into a business rule: money is captured after stock is reserved. That decision in turn needs an answer for a slow payment provider; the scenario "if the payment provider doesn't answer within 5 seconds, the order becomes PaymentPending", which we wrote in the Introduction to Architecture series post "Quality Attribute Scenarios: Turning 'Make It Fast' into a Measurable Requirement", is the product of this discussion.
Read models show up here too. Linus's picking list (orders that are StockReserved but not yet OrderShipped) and the customer's "My orders" screen. Both are fed by the same events, but each answers a different question. That split is the subject of the upcoming CQRS (Command Query Responsibility Segregation) and Event Sourcing series.
Software Design: From Wall to Code
At the last level we are inside a single bounded context and its team. The notes on the wall turn into types almost one to one: blue stickies become command records, orange stickies become event records, lilac stickies become rules that turn an event into a command. The code below isn't an architecture proposal; it is a compilable photograph of the wall:
namespace Bookstore.Ordering;
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.CreateVersion7());
}
// Blue stickies: commands (the intent of an actor or a policy)
public interface ICommand;
public sealed record PlaceOrder(OrderId OrderId, string Isbn, int Quantity) : ICommand;
public sealed record ReserveStock(OrderId OrderId, string Isbn, int Quantity) : ICommand;
public sealed record CapturePayment(OrderId OrderId) : ICommand;
public sealed record CreateShipment(OrderId OrderId) : ICommand;
public sealed record CancelOrder(OrderId OrderId, string Reason) : ICommand;
// Orange stickies: domain events (past tense, already happened)
public interface IDomainEvent;
public sealed record OrderPlaced(OrderId OrderId, string Isbn, int Quantity) : IDomainEvent;
public sealed record StockReserved(OrderId OrderId) : IDomainEvent;
public sealed record OutOfStock(OrderId OrderId, string Isbn) : IDomainEvent;
public sealed record PaymentCaptured(OrderId OrderId) : IDomainEvent;
public sealed record OrderShipped(OrderId OrderId) : IDomainEvent;
public sealed record OrderCancelled(OrderId OrderId, string Reason) : IDomainEvent;
// Lilac stickies: policies ("whenever X, then Y")
public static class Policies
{
public static ICommand? WhenThen(IDomainEvent @event) => @event switch
{
OrderPlaced e => new ReserveStock(e.OrderId, e.Isbn, e.Quantity),
StockReserved e => new CapturePayment(e.OrderId),
PaymentCaptured e => new CreateShipment(e.OrderId),
OutOfStock e => new CancelOrder(e.OrderId, "OutOfStock"),
_ => null
};
}
It's no accident that the policies are a switch expression. Each policy says "when this event arrives, produce this command" and knows nothing else; how much stock there is or who the payment provider is doesn't concern it. Whatever the arrow between two stickies is on the wall, this line is that arrow in code.
To run the wall, a simple decision logic standing in for the yellow stickies and a loop are enough. The warehouse has three copies of Dune (ISBN (International Standard Book Number) 9780441172719), and two customers want two copies each:
using Bookstore.Ordering;
Dictionary<string, int> stock = new() { ["9780441172719"] = 3 };
Run(new PlaceOrder(OrderId.New(), "9780441172719", 2));
Run(new PlaceOrder(OrderId.New(), "9780441172719", 2));
void Run(PlaceOrder placeOrder)
{
Console.WriteLine($"--- {placeOrder.Quantity} x {placeOrder.Isbn}");
ICommand? command = placeOrder;
while (command is not null)
{
IDomainEvent @event = Handle(command);
Console.WriteLine($"{command.GetType().Name,-15} -> {@event.GetType().Name}");
command = Policies.WhenThen(@event);
}
}
// Simple decision logic standing in for the yellow stickies (aggregate/constraint)
IDomainEvent Handle(ICommand command) => command switch
{
PlaceOrder c => new OrderPlaced(c.OrderId, c.Isbn, c.Quantity),
ReserveStock c when stock[c.Isbn] >= c.Quantity => Reserve(c),
ReserveStock c => new OutOfStock(c.OrderId, c.Isbn),
CapturePayment c => new PaymentCaptured(c.OrderId),
CreateShipment c => new OrderShipped(c.OrderId),
CancelOrder c => new OrderCancelled(c.OrderId, c.Reason),
_ => throw new NotSupportedException(command.GetType().Name)
};
StockReserved Reserve(ReserveStock c)
{
stock[c.Isbn] -= c.Quantity;
return new StockReserved(c.OrderId);
}
--- 2 x 9780441172719
PlaceOrder -> OrderPlaced
ReserveStock -> StockReserved
CapturePayment -> PaymentCaptured
CreateShipment -> OrderShipped
--- 2 x 9780441172719
PlaceOrder -> OrderPlaced
ReserveStock -> OutOfStock
CancelOrder -> OrderCancelled
The second order is exactly where the hot spot on the wall plays out: ReserveStock was rejected, the OutOfStock event triggered the policy we added during Process Modeling, and the order was cancelled before any money was captured. You can read this output as a test scenario as well. Ask the domain expert "if we have three copies of Dune and two customers want two each, what should happen?" and compare the answer with these lines.
The Handle method here is deliberately crude. Stock lives in a Dictionary, payment always succeeds, the ISBN is a plain string. Where the real decision logic goes and which rules it follows is the subject of the series' tactical design part: Isbn and Money value objects in 07, invariants in 08, aggregate boundaries in 09, dispatching events during SaveChanges in 12.
From the Wall to Aggregate Candidates
When you place the yellow (constraint) stickies, the question to ask is: which commands have to protect the same rule? PlaceOrder, CancelOrder, CancelItem and RequestReturn all look at the same questions: what state is the order in, which items does it have, has it shipped? Behind those four commands there is one thing, Order. ReserveStock looks at a completely different question: how many copies of this ISBN are on the shelf and how many are reserved? Behind it stands StockItem.
Because this split is visible on the wall, the next discussion gets easier. Between Order and StockItem there is a policy and an event, not the same cluster of stickies. So placing an order and reserving stock don't have to happen in the same transaction; a delay in between is acceptable, and the business accepted the cost of that delay (cancel on OutOfStock) right there on the wall. In post 09 we revisit this decision with Vaughn Vernon's aggregate rules.
A warning: not every yellow sticky on the wall is a class, and not every colored column is a microservice. The wall gives you candidates and their reasons; classes and boundaries become concrete in code, with tests and, if needed, another session.
Remote Sessions and Alternatives
If the team isn't in the same room, the session can run on an online whiteboard such as Miro or Excalidraw. Unlike a physical wall, a digital board needs some preparation: pin the color legend to the top left of the board, give everyone the same set of sticky colors rather than a color per person, and draw the timeline as a horizontal arrow in advance. Split the session into 90-minute blocks; three hours of chaotic exploration in front of a screen does nobody any good. Have a facilitator and a second person who keeps the board tidy. Keep cameras on; the real yield of a Big Picture is the moment Linus looks at an event and says "that's not how it works".
EventStorming isn't the best tool for every question. If you want to clarify the rules of a single user story, Example Mapping, introduced by Matt Wynne in 2015, is faster: the story on a yellow card, rules on blue cards, examples for each rule on green cards, unanswerable questions on red cards. Twenty minutes of Example Mapping to discuss the edge cases of "at most 10 copies per book" beats an eight-metre roll. If the domain expert thinks in stories rather than stickies, Stefan Hofer and Henning Schwentner's Domain Storytelling feels more natural: you draw a scenario with sentences like "Grace receives the return request and sends the shipping label to the customer", using icons for actors and work objects. The three aren't rivals; EventStorming for the Big Picture, Domain Storytelling to explain one process in depth, Example Mapping for one story's acceptance criteria is a good starting point.
Common Pitfalls
Only developers in the room. EventStorming without domain experts is an expensive meeting where developers write what they already know on colored paper. If Grace and Linus can't come, postpone the session.
Jumping to Software Design too early. If "should this be in the Order aggregate?" comes up in the first hour, stop. If commands and aggregates are placed before the events are clear, the wall turns into a copy of the existing database schema.
Events that smell of CRUD (Create, Read, Update, Delete). OrderUpdated, StatusChanged or OrderSaved never come out of a domain expert's mouth. When you see these names, ask "what changed, and why?"; the answer is usually a real event such as OrderCancelled or ShippingAddressChanged. Technical events like RowInserted don't belong on the wall either.
Trying to solve a hot spot on the spot. If the argument between Ada and Linus takes twenty minutes, the rest of the session is lost. Stick the red note, move on, and at the end of the session schedule a separate conversation for the three most important hot spots.
Treating the wall as documentation. Take a photo, but the timeline itself isn't a living document. What needs to last should live in the code (event and command names), in the glossary and in the context map. Someone looking at the photo six months later can't tell what still holds.
A Small Experiment
★☆☆ Add the missing events for the "returns" region of the wall: the customer sent the book back, the warehouse received it, the book turned out to be damaged. Name each in the glossary's language (Return or Refund?) and note which bounded context it falls into.
★★☆ Add a PaymentFailed event and a "when payment fails, release the stock reservation and cancel the order" policy to the code. If WhenThen has to produce two commands for one event, how does its signature change?
★★★ Run a 45-minute mini Big Picture with your own team: the topic is a user's first purchase in your application. Produce at least five hot spots and three pivotal events, then check whether the regions between the pivotal events line up with the projects or namespaces in your code base today.
The Order, StockItem, Money and Isbn that came off the wall are now waiting to become code. In the next post we enter tactical design: an entity is defined by its identity, a value object by value equality; we turn these two concepts into code with C# records, strongly typed ids and Entity Framework Core (EF Core) 10's complex types.