Event Storming и практики моделирования домена
Предыдущие статьи трека дали словарь: поддомены, контексты, агрегаты, доменные события. Остался неудобный вопрос, который обычно замалчивают:
откуда вы вообще взяли, что этот агрегат называется «Заказ», а граница проходит именно здесь?
Ответ «из требований» — самообман: требования пишет человек, который уже провёл моделирование, просто неявно и в одиночку. Event Storming — способ сделать моделирование явным, групповым и быстрым. Формат придумал Альберто Брандолини в 2013 году, отчаявшись объяснять DDD через UML (оригинальный пост, eventstorming.com).
1. Проблема: знание распределено, а времени нет
Знание о домене никогда не лежит в одной голове: продакт знает, зачем; поддержка знает, что ломается; бухгалтер знает правила, о которых никто не помнит («частичная отгрузка требует отдельного акта»); разработчик знает, что система реально делает («там ночной джоб, он всё переписывает»). Классический способ собрать это — интервью. Разберём их по-инженерному.
Пусть в процессе участвуют k носителей знания. Последовательные интервью — это k встреч
по t часов, то есть 2·k·t человеко-часов. Проблема не в стоимости, а в расхождениях:
противоречие между версиями участников i и j обнаруживается лишь когда аналитик сопоставит
записи — после обеих встреч, а чаще позже, на код-ревью или на проде. Потенциальных пар
C(k,2) = O(k²), и каждая стоит отдельного раунда уточнений: ещё встреча, ещё неделя календаря.
Воркшоп — те же k человек у одной стены T часов: k·T человеко-часов, дороже за раз,
но противоречие всплывает в момент возникновения, потому что оба носителя смотрят на один
стикер и спорят вслух. Расхождения разрешаются параллельно: O(k²) конфликтов схлопываются
за O(1) проходов по стене. Цифры: 8 человек × 4 часа = 32 человеко-часа ≈ неделя одного
разработчика, тогда как одна неверно проведённая граница контекста стоит месяцы — миграция
схемы, распутывание общих таблиц, переписывание интеграций (https://courses.digitable.life/post/ddd/02-bounded-contexts/).
Event Storming — не техника рисования схем, а техника синхронизации людей. Схема вторична.
2. Грамматика стены: словарь и предложение
Нотация нарочно бедная — семь-восемь типов стикеров, различимых по цвету, никаких нотаций UML: правила объясняются за 90 секунд человеку, который никогда не видел диаграмм.
Каноническая «фраза» стены читается слева направо: актор отдаёт команду; агрегат принимает решение и порождает событие; политика реагирует следующей командой; read model снабжает актора и политику данными; внешняя система — источник или получатель за границей.
что видно на момент решения"] --> A["Актор
Покупатель"] A --> C["Команда
Подтвердить оплату"] C --> AG["Агрегат
Платёж"] AG -- "инвариант выполнен" --> E["Событие
Оплата подтверждена"] AG -- "инвариант нарушен" --> HS["Hotspot
а если оплата частичная?"] E --> P["Политика
всякий раз, когда оплата подтверждена"] P --> C2["Команда
Зарезервировать товар"] C2 --> AG2["Агрегат
Резерв на складе"] E --> RM EXT["Внешняя система
Платёжный шлюз"] -.-> E
Три замечания, которые экономят часы споров:
- Событие — только в прошедшем времени. «Оплата подтверждена», а не «Подтверждение оплаты»: это заставляет говорить о фактах, а не о формах ввода и таблицах. Появилась на стене «Форма оплаты» — вы проектируете UI, а не домен.
- Команда и событие — разные стикеры, даже если названия зеркальны. Команда может быть отклонена, событие — нет; пара «Оплатить заказ» / «Оплата отклонена» — самое ценное место на стене, там живёт бизнес-правило.
- Политика — то, что случается без человека. Каждый лиловый стикер завтра станет подписчиком, сагой или cron-джобом. Если политику никто не может назвать по имени — у вас в проде «магия».
3. Три уровня воркшопа
Event Storming — семейство из трёх форматов. Смешивать их в одной сессии — частая ошибка фасилитатора.
| Уровень | Кто и сколько | Стикеры | Итог |
|---|---|---|---|
| Big Picture | 15–30 человек, все отделы, 3–4 часа | события, акторы, hotspots | карта боли и кандидаты в контексты |
| Process Modelling | 6–10 владельцев процесса, 2–4 часа | плюс команды, политики, read models | сквозной сценарий, точки автоматизации |
| Software Design | 3–6 разработчиков, от полудня | плюс агрегаты и границы | агрегаты, события, скелет тестов |
3.1. Big Picture: хаос по расписанию
- Chaotic exploration, 20–40 минут. Все одновременно и молча пишут оранжевые события и лепят на стену куда попало. Молча — принципиально: иначе один громкий человек задаст рамку всем.
- Enforce the timeline. Группа выстраивает события во времени. Всплывают дубликаты («Заказ создан» и «Заказ оформлен» — одно и то же?) — первые находки единого языка (https://courses.digitable.life/post/ddd/01-strategic-design/).
- People and systems. Акторы и внешние системы: видно, где процесс упирается в человека, а где — в чужой API.
- Hotspots. Красные стикеры-ромбы на всё, что вызывает спор или «мы не знаем». Хороший воркшоп кончается не «всё понятно», а «вот двадцать вопросов по убыванию риска».
- Pivotal events и разрезы — см. ниже.
3.2. Опорные события: как стена превращается в контексты
Опорное событие (pivotal event) — точка, где меняется словарь, меняются ответственные, меняется темп процесса (секунды → дни). Разрез по нему даёт гипотезу границы контекста.
Почему это работает как алгоритм: «найти границы» в общем виде — кластеризация графа взаимодействий
с максимизацией связности внутри и минимизацией между, то есть перебор порядка O(n²) пар событий
и хуже. Опорные события дают эвристику: линейный проход O(n) по упорядоченной ленте и разрез там,
где меняется язык — не оптимально, но 80% результата за 5% усилий. Признаки верного разреза: по обе
стороны разные слова для одного объекта («заказ» у продаж — «отгрузка» у склада); события
пересекают границу редко и асинхронно; за каждую сторону отвечает одна команда (иначе
граница спорит с законом Конвея); инварианты не пересекают границу — агрегат целиком внутри
контекста (https://courses.digitable.life/post/ddd/03-tactical-building-blocks/).
3.3. Software Design: от ленты к агрегатам
Правило простое: агрегат — это то, что принимает решение по команде и отвечает за инвариант. Сгруппируйте команды и события по «кто решает» — агрегаты проступят сами, а собранные события одного агрегата дадут готовый жизненный цикл:
Ценность диаграммы не в красоте, а в том, что отсутствующие стрелки — это найденные дыры. «Что происходит, если оплата подтвердилась после отмены?» — у стены этот вопрос задают за две минуты, а в проде находят через полгода по расхождению в бухгалтерии.
4. От стикеров к коду: прямое отображение
Перевод в код почти механический. Ниже Python, но отображение одинаково для любого языка.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from decimal import Decimal
# ── ОРАНЖЕВЫЕ СТИКЕРЫ → доменные события: прошедшее время, неизменяемые ──
@dataclass(frozen=True, slots=True)
class OrderPlaced:
order_id: str
total: Decimal
@dataclass(frozen=True, slots=True)
class PaymentConfirmed:
order_id: str
amount: Decimal
@dataclass(frozen=True, slots=True)
class OrderShipped:
order_id: str
shipped_at: datetime
@dataclass(frozen=True, slots=True)
class OrderCancelled:
order_id: str
reason: str # «Заказ отменён» на стене
# ── СИНИЕ СТИКЕРЫ → команды: повелительное наклонение, могут быть отклонены ──
@dataclass(frozen=True, slots=True)
class CancelOrder:
order_id: str
reason: str
requested_at: datetime
@dataclass(frozen=True, slots=True)
class ReserveStock:
order_id: str
class OrderCannotBeCancelled(Exception):
"""Красный стикер, доживший до кода."""
# ── БОЛЬШОЙ БЛЕДНЫЙ СТИКЕР → агрегат: решает по команде, порождает событие ──
PREMIUM_CANCEL_WINDOW = timedelta(minutes=30)
class Order:
"""Единственное место, где живут переходы состояния заказа."""
def __init__(self, order_id: str) -> None:
self.order_id = order_id
self.status = "draft"
self.is_premium = False
self.shipped_at: datetime | None = None
self.changes: list[object] = []
def apply(self, event: object) -> None:
"""evolve: применить свершившийся факт, без всякой валидации."""
match event:
case OrderPlaced(): self.status = "placed"
case PaymentConfirmed(): self.status = "paid"
case OrderShipped(shipped_at=ts): self.status, self.shipped_at = "shipped", ts
case OrderCancelled(): self.status = "cancelled"
@classmethod
def replay(cls, order_id: str, history: list[object]) -> "Order":
"""Восстановление из истории — ровно та же лента, что висела на стене."""
order = cls(order_id)
for event in history:
order.apply(event)
return order
def cancel(self, cmd: CancelOrder) -> OrderCancelled:
"""decide: проверить инвариант, вернуть событие либо отказать."""
if self.status in ("cancelled", "delivered"):
raise OrderCannotBeCancelled(f"нельзя отменить в статусе {self.status}")
if self.status == "shipped":
in_window = (
self.shipped_at is not None
and cmd.requested_at - self.shipped_at < PREMIUM_CANCEL_WINDOW
)
if not (self.is_premium and in_window):
raise OrderCannotBeCancelled(
"после отгрузки отмена доступна только Premium в течение 30 минут"
)
event = OrderCancelled(self.order_id, cmd.reason)
self.apply(event)
self.changes.append(event)
return event
# ── ЛИЛОВЫЕ СТИКЕРЫ → политики: чистая функция «событие → команды» ──
def when_payment_confirmed(event: PaymentConfirmed) -> list[object]:
"""Всякий раз, когда оплата подтверждена, — зарезервировать товар."""
return [ReserveStock(order_id=event.order_id)]
| Стикер на стене | Конструкция в коде | Роль в тесте |
|---|---|---|
| Оранжевое событие | неизменяемый @dataclass(frozen=True) |
Given и Then |
| Синяя команда | @dataclass + метод агрегата |
When |
| Большой бледный агрегат | класс с decide / apply |
subject under test |
| Лиловая политика | функция event → list[command] |
отдельный юнит-тест |
| Красный hotspot | TODO с номером ADR |
падающий тест-заглушка |
4.1. Стена становится исполняемой спецификацией
Тесты пишутся словами со стены: Given (прошлые события) → When (команда) → Then (событие или отказ).
import pytest
SHIPPED_AT = datetime(2026, 7, 16, 12, 0)
def shipped_order(order_id: str, premium: bool) -> Order:
"""Given — левая часть ленты: события, которые уже случились."""
order = Order.replay(order_id, [OrderPlaced(order_id, Decimal("4990.00")),
PaymentConfirmed(order_id, Decimal("4990.00")),
OrderShipped(order_id, SHIPPED_AT)])
order.is_premium = premium
return order
def test_premium_can_cancel_within_30_minutes_after_shipping():
order = shipped_order("ord-1", premium=True) # Given
event = order.cancel( # When
CancelOrder("ord-1", "передумал", SHIPPED_AT + timedelta(minutes=20)))
assert isinstance(event, OrderCancelled) and order.status == "cancelled" # Then
def test_regular_customer_cannot_cancel_after_shipping():
order = shipped_order("ord-2", premium=False)
with pytest.raises(OrderCannotBeCancelled):
order.cancel(CancelOrder("ord-2", "передумал", SHIPPED_AT + timedelta(minutes=5)))
Сложность теста: O(h) по времени, где h — длина истории, и O(1) по внешним зависимостям —
ни базы, ни HTTP, ни моков. Воркшоп окупается дважды: он даёт не только модель, но и готовый
список тест-кейсов на языке, который бизнес может прочитать и оспорить.
4.2. Политика через границу контекста
Лиловый стикер, стоявший между двумя разрезами, в коде превращается в межконтекстную интеграцию:
Ветка alt — ровно тот красный стикер, который повесили со словами «а если товара нет?».
Без воркшопа она появляется в коде через полгода, после первого инцидента. Надёжная доставка таких
событий (outbox, идемпотентность, версии) — в https://courses.digitable.life/post/ddd/05-domain-events-and-integration/.
5. Фасилитация: что реально решает исход
Материалы, пространство, состав. Рулон бумаги 6–8 метров — не прихоть: ограниченная доска заставляет экономить стикеры и терять детали. Стульев быть не должно, движение к стене и есть механизм вовлечения. Правило Брандолини: нужны те, кто задаёт вопросы, и те, кто знает ответы — отсутствие одного «скучного» бухгалтера обесценивает четыре часа, половина hotspots останется неразрешённой. Потолок — около 30 человек, дальше группа перестаёт быть одной группой. Роль фасилитатора — не рисовать, а управлять энергией (перерыв каждые 45–60 минут), пресекать преждевременное проектирование («тут нужен Kafka» → красный стикер и дальше), не давать доминировать самому громкому и, главное, не отвечать на вопросы самому — даже если знает ответ.
Удалённый формат. Miro/Mural платят три налога: нет физического движения, нет периферийного зрения, доска бесконечна — никто не чувствует стоимости стикера. Компенсации: группы по 8–10 человек, сессии по 90 минут, жёсткий таймбокс на шаг, включённые камеры, размеченный заранее шаблон (Miro, шпаргалка DDD Crew).
Что сохранять после. Фото стены — плохой артефакт: через месяц его никто не читает. Сохраняйте в репозитории, рядом с кодом, три вещи:
glossary: # 1. единый язык — идёт в README контекста и в имена классов
- {term: "Резерв", definition: "Обещание склада отгрузить; не то же, что остаток"}
hotspots: # 2. вопросы без владельца и срока не считаются зафиксированными
- id: HS-01
question: "Кто платит за доставку при частичной отгрузке?"
owner: "финансы"
risk: high
resolution: "ADR-014: удерживаем из компенсации продавцу"
context_candidates: # 3. вход для карты контекстов
- name: "Оформление и оплата"
core: true
events_out: ["ОплатаПодтверждена", "ОплатаОтклонена"]
Дальше это превращается в Bounded Context Canvas и карту контекстов из https://courses.digitable.life/post/ddd/02-bounded-contexts/.
6. Типичные ошибки
Стена вместо решения. Воркшоп закончился, фото в Confluence, ничего не изменилось. Лечение: заранее договоритесь, что решается по итогам — границы сервисов? бэклог квартала? приоритет hotspots? Нет решения — не проводите воркшоп.
Проектирование на первом уровне. «Здесь нужен Kafka» на Big Picture убивает участие бизнеса мгновенно; такие реплики переводятся в стикер «обсудить на Software Design».
CRUD-стена. Если на ленте «Создать заказ», «Обновить заказ», «Удалить заказ» — вы описали таблицу, а не домен. Значит, либо домен тривиален (тогда DDD не нужен, https://courses.digitable.life/post/ddd/00-overview/), либо группа боится назвать реальные бизнес-факты.
Одна команда моделирует за всех. Разработчики проводят Event Storming между собой и получают красивую модель собственных заблуждений — ту же анемичную модель, только с оранжевыми стикерами.
Хронология вместо причинности. Лента упорядочена по времени, но соседство трактуют как «А вызывает Б». Соседние события могут быть независимы — а это решает, нужна ли синхронная связь.
Игнорирование hotspots и слишком детальный Big Picture. Первое теряет самый ценный выход воркшопа; второе видно по замершей ленте — двадцать пять человек три часа спорят о названии поля.
Более широкий разбор провалов внедрения — в https://courses.digitable.life/post/ddd/08-ddd-pitfalls/.
7. Соседние техники: когда Event Storming не лучший выбор
Слепые зоны формата: он плохо показывает роли и передачу артефактов между людьми, плохо работает с правилами уровня «а какие ещё бывают случаи» и не даёт спецификации UI.
- Domain Storytelling (domainstorytelling.org) — пиктограммная запись «кто кому что передаёт». Незаменима, где домен про людей и документы: страхование, госуслуги, логистика. Берите, когда ES тонет в вопросе «а кто это делает?».
- Example Mapping (Мэтт Уинн) — 25 минут на одну story: правило → примеры → вопросы. ES даёт карту, а он — глубину правила.
- Event Modeling (eventmodeling.org) — строгий формат с дорожками wireframe / command / event / read model; нужен, когда модель ясна и требуется спецификация UI.
- User Story Mapping (Паттон) — про объём релиза, а не про домен; Core Domain Charts (DDD Crew) — куда вкладывать моделирование (https://courses.digitable.life/post/ddd/01-strategic-design/).
Маршрут, формализованный в DDD Starter Modelling Process: Big Picture → Core Domain Charts → Process Modelling по ядру → Software Design → Example Mapping по каждому правилу → код и тесты; параллельно hotspots превращаются в ADR. Через два-три месяца — повтор по изменившейся части: моделирование не проектный этап, а привычка.
8. Как это применяют в проде
- Короткий воркшоп перед каждой крупной фичей — не четыре часа со всей компанией, а 60–90 минут Process Modelling силами шести человек: переделки посреди спринта исчезают.
- Стена как онбординг — за час новичок поймёт бизнес глубже, чем за неделю чтения кода.
- Имена событий переносятся в код дословно. «Оплата подтверждена» →
PaymentConfirmedв коде →payments.payment-confirmed.v1в топике → то же слово в логах. Это и есть работающий единый язык; расхождение имён — первый признак, что модель поплыла. - Hotspots становятся ADR — иначе через месяц спор повторится с нуля.
- Распил монолита. Самый частый промышленный сценарий: лента событий монолита → опорные события → кандидаты в контексты → порядок извлечения сервисов по критерию «меньше всего пересечений по данным». Надёжнее, чем резать по слоям или по таблицам БД.
Честное ограничение: формат требует доверия. Если есть политический интерес скрывать проблемы, стена покажет отполированную версию процесса — ES лишь сделает это видимым.
Мини-итог
- Event Storming — техника синхронизации знания, а не рисования диаграмм; схема вторична.
- Экономика: воркшоп стоит
O(k·T)человеко-часов, но схлопываетO(k²)расхождений за один проход. - Грамматика: актор → команда → агрегат → событие → политика → команда; событие всегда в прошедшем времени, политика — то, что происходит без человека.
- Три уровня — Big Picture, Process Modelling, Software Design — не смешивать в одной сессии; опорные события дают линейную эвристику поиска границ вместо квадратичного перебора.
- Стикеры отображаются в код один в один, а самый ценный выход воркшопа — список hotspots.
Источники
- Alberto Brandolini. Introducing EventStorming — leanpub.com/introducing_eventstorming, eventstorming.com, пост 2013 года.
- Vlad Khononov. Learning Domain-Driven Design (O’Reilly, 2021) — глава про EventStorming с пошаговым разбором фасилитации; Eric Evans, DDD Reference.
- Hofer, Schwentner. Domain Storytelling — domainstorytelling.org, инструмент Egon.io; Adam Dymitruk, Event Modeling.
- Matt Wynne. Introducing Example Mapping; Jeff Patton. User Story Mapping.
- DDD Crew: Starter Modelling Process, Glossary Cheat Sheet, Bounded Context Canvas, Core Domain Charts.
- Percival & Gregory. Architecture Patterns with Python (cosmicpython.com) — события со стены как message bus и юнит-тесты без базы.
Что дальше
Стена дала модель, глоссарий и список тестов. Осталось собрать всё в работающую систему: агрегат с инвариантами, репозиторий, единица работы, доменные события, слой приложения и HTTP-API — сквозным примером, где виден каждый переход от стикера к строчке кода.