Event Sourcing¶
Раздел 01 · Уровни и общие гарантии
Тип материала: архитектурный разбор. Схемы и примеры иллюстративные, если не указано иное.
Уровень: authoritative persistence доменной модели. Проверка источников: 2026-11-02.
1. Назначение и решаемая проблема¶
Event Sourcing хранит принятые изменения домена как authoritative последовательность событий. Текущее состояние восстанавливается применением этой последовательности. Он полезен, когда история решений, воспроизведение и явные transitions важнее простоты CRUD.
Audit log рядом с mutable balance не делает систему Event Sourcing: если потеря audit не мешает восстановлению authoritative state, источником истины остаётся другая модель. Double-entry ledger может быть реализован разными способами; его бухгалтерские invariants сами по себе не доказывают ES.
2. Принцип работы¶
Command handler загружает aggregate state из snapshot и последующих событий, проверяет business rules и append-ит новые events с expected version. Это optimistic concurrency boundary для данного aggregate. После commit обновляются projections.
События описывают принятые факты: FundsReserved, ReservationReleased, BetSettled. Не следует использовать их как набор вызовов, которые при replay снова обращаются к внешнему PSP. Внешние ответы и nondeterministic decisions нужно сохранять как входные факты, чтобы replay не принимал решение заново.
3. Архитектурная схема¶
flowchart TB
C["Command с request_id"] --> H["Handler: invariants и expected version"]
E["Authoritative event store"] --> H
N["Snapshot версии N"] --> H
H --> A["Atomic append при совпадении версии"]
A --> E
E --> P["Projector / replay"]
P --> V["Read model"]
E --> O["Доставка integration events"]
4. Walkthrough движения данных¶
- Запрос reserve содержит request_id и wallet aggregate ID. Handler восстанавливает state и узнаёт текущую aggregate_version.
- Он проверяет доступный баланс и уже обработанные request IDs. Эти проверки должны относиться к тому же concurrency boundary, что append.
- Handler создаёт FundsReserved и append-ит event при expected_version=N. Два concurrent reserve не могут оба успешно опереться на один N.
- Event store фиксирует новую версию; transport integration events обеспечивает отдельный надёжный delivery path.
- Projector строит историю и доступные для чтения summaries. Его лаг не изменяет факт принятого reserve.
- Для rebuild создаётся новое read generation, выполняется replay до позиции C и catch-up. Никакие старые Reserve команды не принимаются заново.
- Ошибочная проводка исправляется новым reversal/correction event со ссылкой на исходную, сохраняя аудит.
Если операция касается двух wallet aggregates, optimistic append одного stream не обеспечивает atomic transfer. Нужен общий transaction boundary, ledger-owner с атомарными postings или явно спроектированный workflow.
5. Технологии реализации¶
Варианты: специализированный event store либо SQL-таблица с ограничением UNIQUE(aggregate_id, version), atomic append и надёжным чтением по позиции. PostgreSQL может реализовать это как приложение; Kafka topic без нужного expected-version protocol не является автоматическим event store.
Snapshot хранит aggregate_id, version, state schema и checksum. Upcaster или version-aware handlers читают старые schemas. Проекции допустимо хранить в другой СУБД. Snapshot — ускорение восстановления; его можно пересоздать из полной истории, если это действительно поддержано.
6. Консистентность и гарантии обработки¶
Локальная атомарность append и expected-version validation защищает aggregate, но не все cross-aggregate invariants. Replay deterministic только при сохранённых необходимых inputs, ordering и совместимой интерпретации событий.
Idempotency команды отличается от dedup доставки event: один клиентский request может ошибочно породить два разных event_id, если request_id не защищён в append boundary. Projector inbox/dedup не исправит такую ошибку на source.
Exactly-once business effect внешнего PSP требует idempotent request и reconciliation. Проигрывание event store не должно повторно совершать операции. Fowler: Event Sourcing, gateways и replay описывает саму проблему отделения replay от внешних систем.
7. Отказы и восстановление¶
| Сбой | Recovery | Корректность |
|---|---|---|
| Два commands на version N | Один append выигрывает, другой перечитывает и повторно проверяет invariant | Нельзя blind retry старого решения |
| Crash после append до ответа | Найти результат по request_id | Не создавать второе событие |
| Snapshot повреждён | Проверить version/schema, восстановить из events | Snapshot не должен менять truth |
| Новый handler не понимает старый event | Upcaster/совместимый reader и тестовый replay | Не переписывать историю молча |
| Side effect выполнен, ответ потерян | Provider lookup/idempotency и reconciliation | Timeout не означает «ничего не произошло» |
Privacy требует заранее определить, где хранятся PII и как выполняются deletion/legal retention requirements. Immutable не означает право бессрочного хранения персональных данных.
8. Масштабирование и узкие места¶
Частый aggregate становится точкой сериализации. Разбить его можно только если invariants допускают независимые части; wallet нельзя безопасно «ускорить» случайным partitioning balance updates.
Long streams требуют snapshots, а новые проекции — resource budget для replay. Versioning schemas и справочников часто сложнее самого event append. Storage должен поддерживать backup/recovery event history; повреждение authoritative log опаснее потери любой пересоздаваемой projection.
9. Преимущества и недостатки¶
Плюсы: причинная история, возможность построить новые projections, воспроизвести accepted decisions и объяснить corrections. Минусы: сложная evolution, privacy, сложность cross-aggregate transactions и высокая цена ошибочного event design.
Для простой таблицы настроек ES часто создаёт непропорциональную стоимость. История нужна многим системам, но не всякая потребность в audit требует authoritative event model.
10. Альтернативные решения¶
CRUD + transactional audit проще при стандартных update queries. CDC фиксирует изменения существующих tables, сохраняя их источником истины. CQRS разделяет модели независимо от persistence.
Event Sourcing vs CDC — подробное сравнение. ES и CDC могут сочетаться: CDC публикует изменения SQL event store или outbox, не заменяя смысл событий.
11. Реальные примеры и границы подтверждения¶
FACT: experience report о LMAX от 2011-07-12 прямо описывает in-memory Business Logic Processor и Event Sourcing. Это исторический финансовый пример, основанный на рассказе о системе; текущая реализация компании UNKNOWN. Martin Fowler: The LMAX Architecture.
INFERENCE: это подтверждает применимость ES к trading domain, но не универсальность single-thread model и не достижимость чужих performance results в другом проекте. Числа LMAX здесь намеренно не используются.
12. Применение в FinTech и iGaming¶
FinTech: accepted wallet transitions и explanation of postings, если transaction boundaries защищают финансовые invariants. Для transfer лучше сначала определить ledger owner, а затем выбирать ES/CRUD.
iGaming: принятая ставка хранит quote/terms version, settlement и re-settlement — отдельные transitions. Новый официальный результат не стирает прошлую выплату: остаётся связь correction → previous revision → compensating ledger posting.
13. Типичные ошибки проектирования¶
- Считать audit таблицу автоматически authoritative event store.
- Переисполнять команды и внешние HTTP calls при replay.
- Считать event_id достаточной idempotency команд.
- Обещать atomic transfer через два независимых aggregate append.
- Сохранять только snapshot и удалять единственную историю, продолжая обещать произвольный replay.
14. Вопросы технического интервью с ответами¶
Чем событие отличается от команды? Команду можно отклонить; событие фиксирует принятое изменение. Replay применяет факты, а не повторно решает, принимать ли команду.
Защищает ли expected version от двойного списания? Он сериализует конкурирующие изменения выбранного aggregate при корректном append. Дополнительно нужны request idempotency и правильный scope invariant.
Можно ли менять прошлые события? Исправление business fact обычно оформляют новым событием. Миграция хранения/PII — отдельная контролируемая операция с audit и совместимостью readers.
15. Практическое упражнение¶
HYPOTHETICAL: available=100; два concurrent requests reserve по 80 читают version 7. Опишите append decisions и результат повторной проверки проигравшего.
Ожидается один successful reserve, available=20 и rejection второго при отсутствии других изменений. Затем доставьте успешный request повторно и перестройте read model: новых financial events быть не должно. Добавьте correction и объясните, почему rebuild не вызывает реальный payout.