Event Sourcing: когда история важнее текущего состояния

Что такое event sourcing, какие задачи он решает лучше классического CRUD, где усложняет без пользы и на что смотреть перед внедрением

В классическом CRUD-подходе система хранит текущее состояние: запись обновляется, старое значение теряется. Это просто, понятно и в большинстве случаев достаточно. Но есть задачи, где важно не только «что сейчас», а «как мы сюда пришли».

Event sourcing меняет модель хранения: вместо текущего состояния система хранит упорядоченную последовательность событий. Текущее состояние — это результат применения всех событий от начала до конца. Звучит элегантно, но на практике добавляет заметную сложность, и без чёткого понимания задачи эта сложность становится бессмысленной.

В этой статье разберём, когда event sourcing действительно решает проблему, а когда превращается в дорогой способ делать то, что прекрасно решалось обычной таблицей.

В статье

Что такое event sourcing и чем он отличается от лога изменений

Event sourcing — это не просто «логировать все изменения». Ключевое отличие: события — это единственный источник правды. Текущее состояние не хранится отдельно, а вычисляется из событий.

Простой лог изменений (audit log) — дополнение к основной таблице. Его можно потерять, и система продолжит работать. В event sourcing потеря событий — потеря данных.

Событие в event sourcing:

  • описывает факт, который уже произошёл (прошедшее время: ResourceConsumed, TemperatureThresholdExceeded);
  • неизменяемо — его нельзя обновить или удалить;
  • содержит всю информацию, необходимую для воспроизведения изменения;
  • упорядочено по времени внутри агрегата.

Когда event sourcing действительно нужен

Event sourcing оправдан, когда:

  • аудит и compliance — нужна полная, неизменяемая история всех действий (финансы, медицина, регулируемые отрасли);
  • сложные бизнес-правила зависят от истории — не только «сколько на счёте», а «какие операции привели к этому балансу»;
  • temporal queries — вопросы вида «каково было состояние системы на момент X»;
  • отладка и воспроизведение — возможность replay событий для диагностики проблем;
  • event-driven интеграция — события естественно ложатся в основу интеграции между сервисами (но наружу публикуются не сырые внутренние события, а специально оформленные integration events — см. контракты событий).

Event sourcing не нужен, если:

  • текущего состояния достаточно для всех решений;
  • история изменений нужна «на всякий случай» — для этого хватит audit log;
  • команда не готова работать с eventual consistency и проекциями;
  • домен простой и CRUD закрывает все потребности.

Анатомия event store

Event store — это append-only хранилище событий. Минимальная структура записи:

  • stream_id — идентификатор агрегата (например, ID ресурса);
  • version — порядковый номер события в стриме;
  • event_type — тип события (ResourceConsumed, ReserveAllocated);
  • data — полезная нагрузка (JSON/protobuf);
  • metadata — кто, когда, correlation ID;
  • created_at — timestamp.

Ключевой инвариант: в рамках одного стрима события строго упорядочены. Оптимистическая блокировка через version — при записи указываем ожидаемую версию, при конфликте получаем ошибку.

Реализации: EventStoreDB (специализированное), PostgreSQL с append-only таблицей (достаточно для многих случаев), Kafka (может быть event store, но с оговорками: нет нативной оптимистической блокировки (optimistic concurrency) на уровне стрима, а ключи, retention, replay и версионирование событий нужно спроектировать отдельно; partition ≠ stream, поэтому историю одного агрегата так просто не прочитать).

Проекции: как получить текущее состояние

Раз состояние не хранится напрямую, его нужно вычислять. Проекция — это read-модель, построенная из событий.

Два подхода:

Синхронная проекция (in-memory replay): при каждом запросе читаем все события стрима и применяем их. Просто, но медленно для длинных стримов.

Асинхронная проекция (materialized view): отдельный процесс читает поток событий и обновляет read-модель (таблицу, кеш, поисковый индекс). Быстрые чтения, но eventual consistency.

Проекций может быть несколько — каждая оптимизирована под свой сценарий чтения. Это одно из главных преимуществ event sourcing: write-модель и read-модель разделены.

Снэпшоты: зачем и когда

Если агрегат накопил тысячи событий, replay при каждом чтении становится дорогим. Снэпшот — это сохранённое состояние на определённой версии. При загрузке: берём снэпшот + применяем только события после него.

Снэпшоты — это оптимизация, а не обязательная часть. Многие системы прекрасно работают без них, если стримы не слишком длинные.

Где event sourcing усложняет без пользы

Типичные ситуации, когда event sourcing создаёт больше проблем, чем решает:

  • CRUD-домен: каталог товаров, справочники, настройки пользователя — текущего состояния достаточно;
  • команда не готова: eventual consistency, проекции, версионирование событий — это заметный сдвиг в мышлении;
  • нет чётких требований к истории: «может пригодиться» — не причина, а путь к переусложнению;
  • одновременная запись и чтение в одну модель: если нужна строгая консистентность на чтение, проекции добавят задержку.

Практический пример: учёт ресурсов лунной базы

В кейсе лунной базы event sourcing может быть уместен для критичных ресурсов.

Вместо записи «кислорода осталось 847 единиц» хранится цепочка событий:

  • OxygenDelivered { amount: 1000, source: "cargo-7" }
  • OxygenConsumed { amount: 120, module: "habitat-A", period: "2026-06-10T00:00/06:00" }
  • OxygenReserveAllocated { amount: 33, reason: "emergency-protocol" }

Это позволяет:

  • восстановить состояние на любой момент времени;
  • понять, почему запас упал ниже порога;
  • воспроизвести сценарий для анализа;
  • строить разные проекции: текущий баланс, динамика расхода, прогноз исчерпания.

Типичные ошибки при внедрении

  1. Слишком крупные события — событие должно описывать один факт, а не «всё что произошло за транзакцию».
  2. Мутабельные события — событие неизменяемо. Если нужно исправить ошибку — добавляется компенсирующее событие.
  3. Игнорирование версионирования — формат событий меняется со временем. Без стратегии миграции (upcasting) старые события станут нечитаемыми. Подробно — в статье про контракты событий.
  4. Event sourcing везде — не каждый агрегат в системе нуждается в event sourcing. Смешивать подходы в рамках одной системы — нормально.
  5. Путаница event sourcing и event-driven architecture — это разные вещи. Event-driven — про взаимодействие между компонентами. Event sourcing — про хранение состояния.
  6. Игнорирование права на забвение — неизменяемость событий конфликтует с требованием удалить персональные данные (GDPR, 152-ФЗ). Физически вырезать событие из стрима нельзя, не сломав проекции и инвариант версий.

Право на забвение в immutable-хранилище

Это не теоретическая придирка, а первый вопрос комплаенса к event sourcing. Раз события неизменяемы, «просто удалить» персональные данные не получится. Рабочие подходы:

  • crypto-shredding — персональные поля шифруются индивидуальным ключом, ключ хранится отдельно; «удаление» = уничтожение ключа, событие остаётся, но данные нечитаемы;
  • вынос PII за пределы event store — в событии лежит только ссылка (ID), сами персональные данные — в отдельном мутабельном хранилище, которое можно чистить;
  • компенсирующее событие с маскированием — годится для отображения, но не закрывает требование физического удаления.

Если домен затрагивает персональные данные, стратегию забвения нужно заложить до первого события, а не после запроса регулятора.

Checklist: стоит ли вам использовать event sourcing

  1. Есть ли реальное требование к полной истории изменений (не «может пригодиться»)?
  2. Зависят ли бизнес-правила от последовательности прошлых событий?
  3. Нужны ли temporal queries — «каково было состояние на момент X»?
  4. Готова ли команда работать с eventual consistency и проекциями?
  5. Достаточно ли сложен домен, чтобы оправдать дополнительную инфраструктуру?
  6. Есть ли план версионирования событий?
  7. Если в событиях есть персональные данные — продумана ли стратегия их удаления (crypto-shredding, вынос PII)?

Если на большинство вопросов ответ «нет» — скорее всего, хватит CRUD с audit log.

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии