В классическом CRUD-подходе система хранит текущее состояние: запись обновляется, старое значение теряется. Это просто, понятно и в большинстве случаев достаточно. Но есть задачи, где важно не только «что сейчас», а «как мы сюда пришли».
Event sourcing меняет модель хранения: вместо текущего состояния система хранит упорядоченную последовательность событий. Текущее состояние — это результат применения всех событий от начала до конца. Звучит элегантно, но на практике добавляет заметную сложность, и без чёткого понимания задачи эта сложность становится бессмысленной.
В этой статье разберём, когда event sourcing действительно решает проблему, а когда превращается в дорогой способ делать то, что прекрасно решалось обычной таблицей.
В статье
- Что такое event sourcing и чем он отличается от лога изменений
- Когда event sourcing действительно нужен
- Анатомия event store
- Проекции: как получить текущее состояние
- Снэпшоты: зачем и когда
- Где event sourcing усложняет без пользы
- Практический пример: учёт ресурсов лунной базы
- Типичные ошибки при внедрении
- Право на забвение в immutable-хранилище
- Checklist: стоит ли вам использовать 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" }
Это позволяет:
- восстановить состояние на любой момент времени;
- понять, почему запас упал ниже порога;
- воспроизвести сценарий для анализа;
- строить разные проекции: текущий баланс, динамика расхода, прогноз исчерпания.
Типичные ошибки при внедрении
- Слишком крупные события — событие должно описывать один факт, а не «всё что произошло за транзакцию».
- Мутабельные события — событие неизменяемо. Если нужно исправить ошибку — добавляется компенсирующее событие.
- Игнорирование версионирования — формат событий меняется со временем. Без стратегии миграции (upcasting) старые события станут нечитаемыми. Подробно — в статье про контракты событий.
- Event sourcing везде — не каждый агрегат в системе нуждается в event sourcing. Смешивать подходы в рамках одной системы — нормально.
- Путаница event sourcing и event-driven architecture — это разные вещи. Event-driven — про взаимодействие между компонентами. Event sourcing — про хранение состояния.
- Игнорирование права на забвение — неизменяемость событий конфликтует с требованием удалить персональные данные (GDPR, 152-ФЗ). Физически вырезать событие из стрима нельзя, не сломав проекции и инвариант версий.
Право на забвение в immutable-хранилище
Это не теоретическая придирка, а первый вопрос комплаенса к event sourcing. Раз события неизменяемы, «просто удалить» персональные данные не получится. Рабочие подходы:
- crypto-shredding — персональные поля шифруются индивидуальным ключом, ключ хранится отдельно; «удаление» = уничтожение ключа, событие остаётся, но данные нечитаемы;
- вынос PII за пределы event store — в событии лежит только ссылка (ID), сами персональные данные — в отдельном мутабельном хранилище, которое можно чистить;
- компенсирующее событие с маскированием — годится для отображения, но не закрывает требование физического удаления.
Если домен затрагивает персональные данные, стратегию забвения нужно заложить до первого события, а не после запроса регулятора.
Checklist: стоит ли вам использовать event sourcing
- Есть ли реальное требование к полной истории изменений (не «может пригодиться»)?
- Зависят ли бизнес-правила от последовательности прошлых событий?
- Нужны ли temporal queries — «каково было состояние на момент X»?
- Готова ли команда работать с eventual consistency и проекциями?
- Достаточно ли сложен домен, чтобы оправдать дополнительную инфраструктуру?
- Есть ли план версионирования событий?
- Если в событиях есть персональные данные — продумана ли стратегия их удаления (crypto-shredding, вынос PII)?
Если на большинство вопросов ответ «нет» — скорее всего, хватит CRUD с audit log.
Документация и первоисточники
- Канон: Martin Fowler — Event Sourcing, Greg Young — CQRS Documents (PDF), Microsoft — Event Sourcing pattern.
- Продукт/event stores: EventStoreDB, Marten (Postgres, .NET).
- Смежное на сайте: Event Sourcing на практикеготовится, с 10 августа (event store, проекции, снапшоты, upcasting), CQRS и CQRS на практикеготовится, с 17 августа, гарантии доставки и идемпотентность (идемпотентное применение событий), тактический DDD (domain events)Скоро, Outbox, матрица решенийготовится, с 8 августа.
Комментарии