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).

Натискаючи кнопку, ви погоджуєтесь на обробку даних.