Event Sourcing: основы
Что такое Event Sourcing
Event Sourcing (ES) — это способ хранения состояния доменных объектов не в виде «текущей строки», а как неизменяемый журнал событий, описывающих все, что происходило. Текущее состояние агрегата получается сверткой (replay) его событий, а любые представления для чтения строятся как проекции поверх этого журнала.
Ключевые следствия:- История — «первичный источник истины», состояние — проекция истории.
- Любое состояние можно воспроизвести заново, проверить и объяснить (аудит).
- Добавление новых представлений и аналитики не требует миграций старых «снимков» — достаточно проиграть события.
Базовые термины
Агрегат — доменная единица согласованности с четкими инвариантами (Order, Payment, UserBalance).
Событие — неизменяемый факт, произошедший в прошлом (`payment.authorized`, `order.shipped`).
Event Store — аппенд-онли журнал, обеспечивающий порядок событий в пределах агрегата.
Версия агрегата — номер последнего примененного события (для optimistic concurrency).
Снапшот — периодический слепок состояния для ускорения свертки.
Проекция (read-модель) — материализованный вид для чтения/поиска/отчетности (часто — асинхронный).
Как это работает (поток команд → событий → проекций)
1. Клиент отправляет команду (`CapturePayment`, `PlaceOrder`).
2. Агрегат валидирует инварианты и, если все ок, порождает события.
3. События атомарно добавляются в Event Store с проверкой версии (optimistic concurrency).
4. Процессоры проекций подписаны на поток событий и обновляют read-модели.
5. При загрузке агрегата для следующей команды состояние восстанавливается: снапшот (если есть) → события после снапшота.
Дизайн событий
Обязательные атрибуты (ядро)
json
{
"event_id": "uuid",
"event_type": "payment. authorized. v1",
"aggregate_type": "Payment",
"aggregate_id": "pay_123",
"aggregate_version": 5,
"occurred_at": "2025-10-31T10:42:03Z",
"payload": { "amount": 1000, "currency": "EUR", "method": "card" },
"meta": { "trace_id": "t-abc", "actor": "user_42" }
}
Рекомендации:
- Именование: `domain.action.v{major}`.
- Аддитивность: новые поля — опциональные, без изменения смысла старых.
- Минимализм: только факты, без дублирования легко восстанавливаемых данных.
Контракты и схемы
Фиксируйте схемы (Avro/JSON Schema/Protobuf) и проверяйте совместимость на CI.
Для «ломающих» изменений — новая мажорная версия события и параллельная публикация `v1`/`v2` на период миграции.
Конкурентный доступ: optimistic concurrency
Правило: запись новых событий возможна только если `expected_version == current_version`.
Псевдокод:pseudo load: snapshot(state, version), then apply events > version new_events = aggregate. handle(command)
append_to_store(aggregate_id, expected_version=current_version, events=new_events)
//if someone has already written an event between load and append, the operation is rejected -> retray with reload
Так мы гарантируем целостность инвариантов без распределенных транзакций.
Снапшоты (ускорение свертки)
Делайте снапшот каждые N событий или по таймеру.
Храните `snapshot_state`, `aggregate_id`, `version`, `created_at`.
Всегда проверяйте и догоняйте события после снапшота (не доверяйте только слепку).
Снимайте снапшоты так, чтобы их можно было пересоздать из лога (не храните «магические» поля).
Проекции и CQRS
ES естественно сочетается с CQRS:- Write-модель = агрегаты + Event Store.
- Read-модели = проекции, обновляемые событиями (Redis карточки, OpenSearch для поиска, ClickHouse/OLAP для отчетов).
- Проекции идемпотентны: повторная обработка того же `event_id` не меняет результат.
Эволюция схем и совместимость
Additive-first: добавляйте поля; не меняйте типы/семантику.
Для сложных изменений: выпускайте новые типы событий и пишите миграторы проекций.
Поддерживайте двойную запись (`v1` + `v2`) на переходный период и снимайте `v1`, когда все проекции готовы.
Безопасность, PII и «право на забвение»
История часто содержит чувствительные данные. Подходы:- Минимизируйте PII в событиях (идентификаторы вместо данных, детали — в защищенных сторонах).
- Крипто-стирание: шифруйте поля и при запросе удаления уничтожайте ключ (событие остается, но данные недоступны).
- События-редакции: `user.piiredacted.v1` с заменой чувствительных полей в проекциях (история сохраняет факт редактирования).
- Политики ретенции: для некоторых доменов часть событий можно архивировать в WORM-хранилища.
Производительность и масштабирование
Партиционирование: порядок важен внутри агрегата — партиционируйте по `aggregate_id`.
Холодный старт: снапшоты + периодическая «уплотняющая» свертка.
Batch-аппенд: группируйте события одной транзакцией.
Backpressure и DLQ для процессоров проекций; измеряйте лаг (время и количество сообщений).
Индексация Event Store: быстрый доступ по `(aggregate_type, aggregate_id)` и по времени.
Тестирование
Specification tests для агрегатов: сценарий «команды → ожидаемые события».
Projection tests: подайте поток событий и проверьте материализованное состояние/индексы.
Replayability tests: пересоберите проекции «с нуля» на стенде — убедитесь, что итог совпадает.
Chaos/latency: инжектируйте задержки и дубли, проверяйте идемпотентность.
Примеры доменов
1) Платежи
События: `payment.initiated`, `payment.authorized`, `payment.captured`, `payment.refunded`.
Инварианты: нельзя `capture` без `authorized`; суммы неотрицательны; валюта неизменна.
Проекции: «карточка платежа» (KV), поиск транзакций (OpenSearch), отчетность (OLAP).
2) Заказы (e-commerce)
События: `order.placed`, `order.paid`, `order.packed`, `order.shipped`, `order.delivered`.
Инварианты: переходы статусов по диаграмме состояний; отмена возможна до `shipped`.
Проекции: список заказов пользователя, SLA-дашборды по статусам.
3) Балансы (финансы/iGaming)
События: `balance.deposited`, `balance.debited`, `balance.credited`, `balance.adjusted`.
Жесткий инвариант: баланс не уходит < 0; команды идемпотентны по `operation_id`.
Критические операции читают прямо из агрегата (строгая согласованность), UI — из проекции (eventual).
Типовая структура Event Store (вариант с БД)
events
`event_id (PK)`, `aggregate_type`, `aggregate_id`, `version`, `occurred_at`, `event_type`, `payload`, `meta`
Индекс: `(aggregate_type, aggregate_id, version)`.
snapshots
`aggregate_type`, `aggregate_id`, `version`, `state`, `created_at`
Индекс: `(aggregate_type, aggregate_id)`.
consumers_offsets
`consumer_id`, `event_id`/`position`, `updated_at` (для проекций и ретлея).
Частые вопросы (FAQ)
Обязательно ли использовать ES везде?
Нет. ES полезен, когда важны аудит, сложные инварианты, воспроизводимость и разные представления данных. Для простого CRUD это избыточно.
Как быть с запросами «актуального состояния»?
Либо читайте из проекции (быстро, eventual), либо — из агрегата (дороже, но строго). Критические операции обычно используют второй путь.
Нужен ли Kafka/стрим-брокер?
Event Store — источник истины; брокер удобен для распространения событий проекторам и внешним системам.
Что делать с «правом на забвение»?
Минимизировать PII, шифровать чувствительные поля и применять крипто-стирание/редакцию в проекциях.
Как мигрировать старые данные?
Напишите скрипт ретроспективной генерации событий («ре-хайстори») или начните с «состояния-как-есть» и публикуйте события только для новых изменений.
Антипаттерны
Event Sourcing «по привычке»: усложняет систему без доменной выгоды.
Fat events: раздутые payload’ы с PII и дублями — тормоза и проблемы комплаенса.
Отсутствие optimistic concurrency: потеря инвариантов при гонках.
Невоспроизводимые проекции: нет реплея/снапшотов → ручные фиксы.
Сырые CDC как доменные события: утечка схем БД и жесткая связность.
Смешивание внутренних и интеграционных событий: наружу публикуйте стабилизированную «витрину».
Чек-лист для продакшена
- Определены агрегаты, инварианты и события (названия, версии, схемы).
- Event Store обеспечивает порядок в пределах агрегата и optimistic concurrency.
- Включены снапшоты и план их пересоздания.
- Проекции идемпотентны, есть DLQ и метрики лага.
- Схемы валидируются на CI, политика версий — документирована.
- PII минимизирована, поля шифруются, есть стратегия «забвения».
- Реплей проекций проверен на стенде; есть план аварийного восстановления.
- Дашборды: скорость аппенда, лаг проекций, ошибки применений, доля ретраев.
Итог
Event Sourcing делает историю системы первоклассным артефактом: мы фиксируем факты, из них воспроизводим состояние и свободно строим любые представления. Это дает аудит, устойчивость к изменениям и гибкость аналитики — при условии дисциплины в схемах, конкурентном контроле и грамотной работе с чувствительными данными.