Перейти к содержанию

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 движения данных

  1. Запрос reserve содержит request_id и wallet aggregate ID. Handler восстанавливает state и узнаёт текущую aggregate_version.
  2. Он проверяет доступный баланс и уже обработанные request IDs. Эти проверки должны относиться к тому же concurrency boundary, что append.
  3. Handler создаёт FundsReserved и append-ит event при expected_version=N. Два concurrent reserve не могут оба успешно опереться на один N.
  4. Event store фиксирует новую версию; transport integration events обеспечивает отдельный надёжный delivery path.
  5. Projector строит историю и доступные для чтения summaries. Его лаг не изменяет факт принятого reserve.
  6. Для rebuild создаётся новое read generation, выполняется replay до позиции C и catch-up. Никакие старые Reserve команды не принимаются заново.
  7. Ошибочная проводка исправляется новым 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.

Дополнительные источники

Event Sourcing; LMAX experience report; Monzo balances.