GH GambleHub

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 делает историю системы первоклассным артефактом: мы фиксируем факты, из них воспроизводим состояние и свободно строим любые представления. Это дает аудит, устойчивость к изменениям и гибкость аналитики — при условии дисциплины в схемах, конкурентном контроле и грамотной работе с чувствительными данными.

Contact

Свяжитесь с нами

Обращайтесь по любым вопросам или за поддержкой.Мы всегда готовы помочь!

Начать интеграцию

Email — обязателен. Telegram или WhatsApp — по желанию.

Ваше имя необязательно
Email необязательно
Тема необязательно
Сообщение необязательно
Telegram необязательно
@
Если укажете Telegram — мы ответим и там, в дополнение к Email.
WhatsApp необязательно
Формат: +код страны и номер (например, +380XXXXXXXXX).

Нажимая кнопку, вы соглашаетесь на обработку данных.