What event storming is and why it helps with service decomposition

Event storming is a workshop technique, created by Alberto Brandolini, in which people map a business process as a timeline of domain events: past-tense facts such as "Order Placed" or "Payment Captured", written on sticky notes along a wall. Commands, actors, policies and external systems are added around those events. It grew out of domain driven design and is a practical way to find bounded contexts: areas where a term keeps one meaning and one model applies.

It helps with service decomposition because it surfaces evidence that code rarely shows. Pivotal events mark where responsibility passes from one part of the process to the next. Shifts in language appear when "order" means a basket to sales and a pick list to the warehouse. Ownership becomes visible when experts dispute who can change a rule. Consistency needs emerge when one step must be immediately consistent with another, while a different step can tolerate delay.

Treat the result as evidence to test, not as a diagram you cut into microservices. A bounded context is a linguistic and model boundary. A service is a deployment decision.

Bounded context Deployable service
Boundary of meaning and model Boundary of runtime and release
Found through language and rules Chosen for scaling, team autonomy, failure isolation
One context → one, several, or zero deployables Can stay a module in a modular monolith

Backend and platform engineers should take part alongside domain experts who know how the work is actually done. A neutral facilitator runs the session, keeps the timeline moving and makes sure disagreements are recorded instead of settled early.

Scoping and preparing a big-picture session

Pick one end-to-end business flow, such as "customer places an order until it is delivered," and write it on the wall before anyone arrives. A narrow scope keeps the timeline readable. Without one, the session drifts into cataloguing the whole company.

Keep the group small enough that everyone can speak, roughly the number of people who can hold one conversation around a wall. Use a long modeling surface, such as a paper roll or a cleared wall. Events need horizontal space to spread out, and a cramped surface hides gaps.

Agree a sticky-note legend up front and post it where everyone can see it. The sample agenda and legend are provided as a config block alongside this guide.

  • Domain events: orange
  • Commands: blue
  • Actors: small yellow
  • Policies: lilac
  • External systems: wide pink
  • Read models: green
  • Hotspots: red, placed at an angle
Aspect Physical wall Remote whiteboard
Spontaneous participation High, because people walk up and add notes Lower, because people wait their turn
Scanning the timeline Step back and see all of it Zooming fragments the view
Record keeping Needs photos Built in

Watch for three facilitation risks. First, debates can stall the session. Mark the disagreement as a hotspot and move on. Second, senior voices can dominate. Ask everyone to place notes silently first. Third, the group may drift into technical design, such as databases or APIs, too early. Park those notes for later.

The model reflects who was in the room. Without domain experts, you get a model of how engineers imagine the business works.

# Big-picture Event Storming session: copy and adapt
# Flow in scope (write on the wall before anyone arrives):
#   "Customer places an order until it is delivered"

[participants]
# Small enough to hold one conversation around the wall
domain_experts = order desk lead, warehouse supervisor, customer support rep
engineers      = 2-3 from the teams that own this flow
facilitator    = 1 (does not model, keeps time and parks topics)

[surface]
# Long and horizontal so events can spread out and gaps stay visible
type = paper roll or cleared wall, several metres long

[legend]
# Post this where everyone can see it before the first note goes up
domain_event    = orange, past tense ("Order Placed")
command         = blue, imperative ("Place Order")
actor           = small yellow
policy          = lilac ("whenever X, then Y")
external_system = wide pink
read_model      = green
hotspot         = red, placed at an angle

[agenda]
# Times are a starting point; adjust to the group
00:00  Scope, legend, ground rules (10 min)
00:10  Silent writing: everyone adds orange events alone (20 min)
00:30  Enforce the timeline: order events left to right, merge duplicates (30 min)
01:00  Break (10 min)
01:10  Walk the flow aloud; mark disagreements as red hotspots (30 min)
01:40  Add commands, actors, policies, external systems, read models (30 min)
02:10  Review hotspots and parked topics; agree owners for follow-up (20 min)

[facilitation_rules]
stalled_debate  = mark it as a hotspot and move on
senior_voices   = place notes silently first, discuss second
tech_design     = databases or APIs go to the parking area, not the timeline
missing_experts = do not run the session; the model will reflect engineers' guesses

A copyable plain-text config block with a sample scope, participant mix, sticky-note legend, timed agenda and facilitation rules for a big-picture Event Storming session.

Running the timeline and finding candidate boundaries

Start with chaotic exploration. Everyone writes domain events on orange stickies, phrased in the past tense: "Order Placed," "Payment Authorized," "Parcel Dispatched." The past tense matters because it forces people to describe facts the business cares about, not features or screens. Don't sort anything yet. Volume beats precision at this stage.

Next, enforce chronology. Arrange the events left to right and remove duplicates. Gaps and contradictions show up quickly: two people place "Invoice Sent" at opposite ends of the wall, or nobody can say what happens after "Return Requested."

Mark those spots with hotspot stickies instead of resolving them on the spot. A debate about refund rules can consume the whole session. A hotspot records the disagreement or unknown and keeps the timeline moving.

Reading the wall for boundaries

Look for these signals:

  • Pivotal events. Some events hand work from one phase to the next, such as "Order Confirmed" passing control from sales to fulfillment. These are natural seams.
  • Shifting language. The same word can mean different things. In sales, an "order" carries prices and discounts. In fulfillment, it is a list of items and an address. Different meanings suggest different bounded contexts.
  • Actor or department changes. When the people acting on events change, ownership usually changes too.
  • Swimlanes. Some groups of events cluster apart from the rest without being forced.

Moving to process level

Add commands (blue), policies of the form "whenever X, then Y" (lilac), and read models (green). Commands and policies show where decisions are made. Read models show which data each decision needs. A boundary that forces one decision to read data owned by several other boundaries is a weak cut.

Worked example: two boundary cuts for an order-to-delivery flow

Illustrative example, not a measured case. The flow below covers a single order. The two candidate cuts are marked A and B.

OrderPlaced → PaymentAuthorized → StockReserved ‖A‖ ItemsPicked → ShipmentDispatched → DeliveryConfirmed OrderPlaced → PaymentAuthorized ‖B‖ StockReserved → ItemsPicked → ShipmentDispatched → DeliveryConfirmed

Cut A follows the data nouns. Ordering owns orders, payments and reservations, and Shipping owns picking and dispatch. Cut B splits at the pivotal event PaymentAuthorized. Ordering owns placement and payment, and Fulfillment owns everything from reservation to delivery.

Test each cut against the invariants that must hold transactionally:

  • Reserved quantity never exceeds on-hand stock. Pickers decrement on-hand stock, and reservations check it. Under Cut A, these two writes live in different services. That forces either a distributed transaction or a shared stock table. Under Cut B, both writes stay inside Fulfillment.
  • An order is charged at most once. Both cuts keep this rule inside Ordering.
  • Delivery status is visible to the customer. This can be eventually consistent, so it can cross a boundary safely as an event.
Cost introduced Cut A Cut B
Cross-boundary transactions 1 (reserve + pick) 0
Synchronous calls 2 (pick checks reservation; dispatch reads order address) 0
Shared data stock levels table none
Integration events crossing StockReserved PaymentAuthorized, DeliveryConfirmed

Cut B has fewer coupling points because each invariant fits inside one context. The events that cross the boundary only report facts that have already happened.

For Cut B, PaymentAuthorized carries orderId, the order lines and the shipping address. Fulfillment never calls back to Ordering for that data. DeliveryConfirmed carries orderId and deliveredAt.

[
  {
    "eventType": "PaymentAuthorized",
    "eventId": "evt-7f3c2a10",
    "occurredAt": "2026-03-14T09:21:05Z",
    "producer": "ordering",
    "schemaVersion": 1,
    "data": {
      "orderId": "ord-10482",
      "lines": [
        { "sku": "SKU-TSHIRT-M-BLUE", "quantity": 2 },
        { "sku": "SKU-MUG-WHITE", "quantity": 1 }
      ],
      "shippingAddress": {
        "name": "Jane Example",
        "line1": "12 Sample Street",
        "city": "Exampleton",
        "postalCode": "00000",
        "country": "GB"
      }
    }
  },
  {
    "eventType": "DeliveryConfirmed",
    "eventId": "evt-b91e44d2",
    "occurredAt": "2026-03-16T15:42:30Z",
    "producer": "fulfillment",
    "schemaVersion": 1,
    "data": {
      "orderId": "ord-10482",
      "shipmentId": "shp-55120",
      "deliveredAt": "2026-03-16T15:40:00Z"
    }
  }
]

Illustrative payloads for the two integration events that cross Cut B. PaymentAuthorized carries the order lines and shipping address so Fulfillment can reserve, pick and dispatch without calling back into Ordering, and DeliveryConfirmed reports a completed fact that Ordering consumes eventually.

Validating candidates against teams and change history

Neither cut is a decision yet. Treat each as a hypothesis and test it against two pieces of evidence the sticky notes cannot show: who will own the code, and how the code has actually changed.

Map candidates to teams

Conway's law cuts both ways. If you draw boundaries around today's org chart, you can freeze its dysfunction into the architecture. A team split for historical reasons becomes a service split nobody can undo. If you ignore team structure, the boundary erodes, because two teams sharing one context will coordinate through the code and leak changes across it. For each candidate context, name one owning team. If you can't, or if one team owns half of two contexts, treat that as a finding, not a detail.

Check co-change history

Run git log --name-only --pretty=format: over a meaningful window. Trim paths to directory depth with cut or awk, then count pairs with sort | uniq -c | sort -rn. Directories that land on opposite sides of a cut but appear together in many commits suggest the cut is wrong.

Read these counts as evidence, not proof. Bulk renames, formatting sweeps and dependency bumps inflate them. Low counts can mean the boundary is good, or that the code is rarely touched at all.

Record decisions

Write down three things:

  • A short context map. Label each relationship: upstream/downstream, shared kernel, or anti-corruption layer.
  • The integration events. List each event explicitly, along with its payload.
  • Unresolved hotspots. Record them as follow-up work with an owner.
#!/usr/bin/env bash
# Count how often pairs of directories change in the same commit.
# A high count for a pair that straddles a candidate boundary is evidence, not proof.
set -euo pipefail

SINCE="${1:-12 months ago}" # window: long enough to span several release cycles
DEPTH="${2:-2}"             # aggregate paths to this depth, e.g. services/billing
MAX_DIRS="${3:-8}"          # skip wide commits (bulk renames, format sweeps, dependency bumps)

# '@@' marks the start of each commit; the file names follow it.
git log --since="$SINCE" --no-merges --name-only --pretty=format:'@@' \
| awk -v depth="$DEPTH" -v max="$MAX_DIRS" '
  # Emit each unordered directory pair touched by one commit, then reset.
  function flush(   a, b, k) {
    k = 0
    for (a in dirs) k++
    if (k > 1 && k <= max)
      for (a in dirs) for (b in dirs) if (a < b) print a " <-> " b
    split("", dirs)   # portable way to clear an awk array
  }
  $0 == "@@" { flush(); next }
  NF == 0     { next }
  {
    # Trim the file path to its directory prefix at the chosen depth.
    n = split($0, p, "/")
    d = (n == 1) ? "." : p[1]
    for (i = 2; i <= depth && i < n; i++) d = d "/" p[i]
    dirs[d] = 1
  }
  END { flush() }
' \
| sort | uniq -c | sort -rn | head -n 25

A shell pipeline that turns git history into ranked co-change counts for directory pairs. It skips merge commits and unusually wide commits, so that sweeping changes do not inflate the evidence for or against a candidate boundary.

When to use it, when to avoid it, and alternatives

Event storming pays off when the decomposition decision is large or unclear, and when a flow crosses several departments that each understand only their own part. In those cases the expensive mistakes come from misread handoffs and policies, and that is exactly what the timeline exposes.

Downweight it for read-heavy, reporting or CRUD-dominant areas. A catalogue admin screen or a reporting warehouse has few meaningful domain events, so its boundaries depend more on who owns the data and which queries must be fast. Skip it for small systems, where the workshop costs more than the decision is worth. Skip it too when domain experts can't attend, because engineers alone will reproduce the current code structure rather than the business process.

Treat the output as candidates, not a deployment plan. Splitting too early or too finely turns in-process calls into network calls and brings distributed failure modes such as timeouts, partial writes and retries that duplicate work. It also forces eventual consistency, which the business may not accept: finance may refuse to show an order as paid before the ledger agrees. A modular monolith is a common first step. Enforce the candidate boundaries as modules with explicit interfaces, and extract a service only when a boundary has held up under real change.

Alternative Best when Limitation
Domain storytelling You need to capture one concrete scenario in detail Narrower view of the whole domain
Data ownership and query analysis Areas are CRUD- or reporting-heavy Misses process and policy boundaries
Change-coupling analysis of the codebase A mature codebase with useful version history exists Reflects past structure, not the intended one
Starting from team topology Team structure is fixed or hard to change May encode org quirks into architecture
Incremental extraction from a modular monolith Boundaries are uncertain Requires discipline to keep modules isolated

Impact on development cost and engineering time

Workshop time is visible on calendars, while the cost of a bad boundary arrives later as rework. To compare them, use one formula for both:

Cost = people × hours

Illustrative estimate (assumed numbers, not a benchmark):

  • Workshop: 8 participants (4 engineers, 3 domain experts, 1 facilitator) × 8 hours (two half-day sessions) = 64 person-hours.
  • Moving one misplaced boundary after launch: 2 engineers × 3 weeks × 40 hours = 240 person-hours.

Under these assumptions, the workshop pays for itself if it prevents about a quarter of one boundary move. Substitute your own team size, session length, and past rework effort. The comparison only holds when the decision is large. For a single new service with an obvious owner, the workshop costs more than the decision is worth.

The savings come from three places:

  • Fewer cross-service transactions. When a business invariant spans two services, someone has to build sagas, compensation logic, and reconciliation jobs.
  • Less rework. Moving a boundary after services and data stores exist means migrating data, running dual writes, and versioning contracts while production traffic continues.
  • Fewer synchronous dependencies. Every call chain you avoid is one less thing to build, give timeouts, monitor, and debug during incidents.

To keep later decisions cheap, treat the event model as a maintained artifact. Store the photographed or digital board next to the architecture decision records. Revisit it when a new business capability arrives or when change history shows two services repeatedly being modified together. Short, targeted follow-up sessions on one region of the timeline cost far less than redoing the whole map.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to event storming service boundaries, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Changing a shared variable is one edit and one review instead of editing every service's .env by hand.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.