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