Event Sourcing на собеседовании системного аналитика
Содержание:
Идея event sourcing
В обычном приложении в базе хранится текущее состояние: строка accounts с полем balance = 120. Каждое изменение перезаписывает старое значение, и история теряется — мы видим итог, но не знаем, как к нему пришли. Event sourcing переворачивает это: источником истины становится не текущее состояние, а неизменяемый журнал событий. Состояние в любой момент выводится (derived) заново проигрыванием событий с начала.
События:
1. AccountOpened {id: 42}
2. Deposited {amount: 100}
3. Deposited {amount: 50}
4. Withdrawn {amount: 30}
Выведенное состояние: balance = 120Ключевая мысль для собеса: события — это факты, которые уже случились, они в прошедшем времени (Deposited, OrderShipped) и не меняются. Текущий баланс — это не то, что мы храним, а то, что мы вычисляем, свернув все события. Отсюда все свойства подхода: полный аудит «из коробки», возможность узнать состояние на любой момент прошлого и восстановить любую производную модель из журнала.
Event store — хранилище событий
Event store — это append-only журнал: события только дописываются в конец и никогда не изменяются и не удаляются. Именно неизменяемость даёт аудит и воспроизводимость.
В качестве хранилища используют:
- EventStoreDB — специализированная БД под event sourcing, с потоками и подписками из коробки.
- Kafka с бесконечным retention — журнал событий как есть, но без удобной выборки по конкретному агрегату.
- Таблицу в PostgreSQL (
event_storeс полями aggregate_id, version, event_type, payload) — самый частый прагматичный вариант. - Apache Pulsar — альтернатива Kafka с похожей моделью лога.
Важные свойства хранилища, которые называют на собесе:
- Append-only. Событие нельзя изменить или удалить — только добавить компенсирующее (например,
OrderCancelled). - Поток на агрегат. События одного заказа образуют отдельный поток (stream): чтобы восстановить заказ, читаем только его события, а не весь журнал.
- Оптимистичная блокировка по версии. У каждого события в потоке есть номер версии. При записи проверяют ожидаемую версию — если её кто-то уже увеличил, значит был конкурентный апдейт, и запись отклоняется. Так решают конфликты без глобальных блокировок.
Снапшоты
Проигрывать все события при каждом чтении дорого: если у агрегата их 100 тысяч, восстанавливать состояние с нуля каждый раз медленно.
Снапшот (snapshot) — это периодически сохранённое состояние агрегата на определённой версии. При чтении берут последний снапшот и доигрывают только события после него.
События 1–1000 → снапшот v1
События 1001–2000 → снапшот v2
Чтение = загрузить снапшот v2 + проиграть события с 2001 до текущегоЭто компромисс: снапшоты занимают место и их надо поддерживать, зато чтение ускоряется. Важный нюанс — снапшот остаётся оптимизацией, а не источником истины: если он повреждён или изменилась логика свёртки, состояние всегда можно восстановить из событий заново.
Проекции
Журнал событий неудобно читать напрямую — под запросы строят проекции (projections), они же read models: денормализованные представления, собранные из событий под конкретные сценарии чтения.
События заказа → проекция "OrderSummary" (денормализованная таблица для карточки заказа)
→ проекция "CustomerHistory" (история покупок клиента)
→ проекция "DailyRevenue" (агрегат выручки по дням)Из одного и того же потока событий строят сколько угодно проекций под разные нужды: одна оптимизирована под карточку заказа, другая — под аналитический отчёт. Это естественно стыкуется с CQRS: запись идёт в журнал событий (command side), а чтение — из проекций (query side). Проекции обновляются асинхронно, поэтому они eventually consistent — отстают от журнала на доли секунды, и это на собесе стоит проговорить как осознанный trade-off.
Replay
Поскольку проекции — производные от журнала, их можно пересобрать в любой момент, перечитав события с начала. Это и называется replay.
Когда он нужен:
- Добавили новую проекцию. Чтобы наполнить её историей, проигрывают весь журнал (backfill).
- Нашли баг в логике проекции. Исправляют код и пересобирают проекцию заново из событий — данные восстанавливаются корректно.
- Изменилась структура read model. Переразложили события в новую схему.
- Аудит и отладка. Можно проиграть события и посмотреть, как формировалось состояние, — расследовать инцидент.
Replay — мощнейшее свойство подхода, но на больших журналах он медленный (миллионы событий надо перечитать), поэтому его комбинируют со снапшотами и запускают на реплике, чтобы не мешать проду.
Когда применять
Подходит, когда:
- Критичен аудит. Банкинг, финансы, комплаенс — нужна неизменяемая история всех изменений.
- Нужны запросы «на момент времени». «Каким было состояние заказа на 2026-04-15» — журнал отвечает на это естественно.
- Много разных read-моделей. Одни данные надо показывать в нескольких представлениях с разной денормализацией.
- У сущности много изменений состояния. Домен с богатым жизненным циклом (заказы, заявки, договоры).
Overkill, когда:
- Простой CRUD. Справочник или настройки — журнал событий тут только усложнит систему.
- Нет требований к аудиту. Если история изменений не нужна, накладные расходы не окупаются.
- Маленькая команда и сжатые сроки. Порог входа высокий: eventual consistency, версионирование событий, пересборка проекций — всё это нужно уметь эксплуатировать.
На собесе за фразу «event sourcing везде» снижают оценку так же, как за «микросервисы для всего»: интервьюер хочет услышать, что вы понимаете цену подхода и применяете его точечно.
Как это спрашивают на собесе
- «Чем event sourcing отличается от обычного хранения состояния?» Обычная модель хранит текущий снимок и перезатирает историю; event sourcing хранит журнал фактов, а состояние выводит проигрыванием. Отсюда аудит и temporal-запросы.
- «Как получить текущее состояние, если у агрегата миллион событий?» Снапшоты плюс доигрывание хвоста после последнего снапшота.
- «Как связаны event sourcing и CQRS?» Это разные, но часто совместные вещи: CQRS разделяет запись и чтение, event sourcing задаёт, как хранится запись (журнал), а проекции обслуживают чтение.
- «Что делать, если поменялась схема события?» Версионировать события и писать upcaster'ы (преобразование старого формата в новый при чтении) — удалять или ломать старые события нельзя.
- «В чём минусы подхода?» Eventual consistency проекций, сложность версионирования событий, порог входа, дороговизна replay на больших журналах.
Интервьюер проверяет не заученное определение, а понимание trade-off: за аудит и гибкость чтения платят сложностью и eventual consistency.
Частые ошибки
Считать event sourcing синонимом CQRS. Это разные концепции. CQRS — разделение команд и запросов; event sourcing — способ хранения через журнал событий. Их часто используют вместе, но можно и по отдельности.
Хранить в событии текущее состояние, а не факт. Событие описывает произошедшее изменение (Deposited {amount: 100}), а не полный снимок баланса. Иначе теряется вся ценность журнала.
Менять или удалять события. Журнал append-only. Отмену моделируют компенсирующим событием (OrderCancelled), а не удалением строки, иначе рушится аудит и воспроизводимость.
Забывать про версионирование событий. Схема событий со временем меняется. Без версий и upcaster'ов старые события перестанут читаться после изменения формата.
Игнорировать eventual consistency проекций. Проекции обновляются асинхронно и отстают от журнала. Если бизнес-логике нужна строгая согласованность «прочитал сразу после записи» — это надо явно проектировать, а не считать данные всегда актуальными.
Тащить подход в простой CRUD. Для справочников и настроек журнал событий — избыточная сложность без выгоды.
Связанные темы
- Domain events для SA
- Event-driven архитектура для SA
- CQRS для SA
- DDD для SA
- Подготовка к собесу системного аналитика
FAQ
Чем event sourcing отличается от обычного хранения состояния?
Обычная модель хранит текущий снимок (строку с полями) и перезаписывает его при каждом изменении — история теряется. Event sourcing хранит неизменяемый журнал событий-фактов, а текущее состояние вычисляет их проигрыванием. Это даёт полный аудит и возможность узнать состояние на любой момент прошлого.
Event sourcing и CQRS — это одно и то же?
Нет. CQRS разделяет модель записи и модель чтения. Event sourcing — способ хранить данные в виде журнала событий. Их часто применяют вместе (запись в журнал, чтение из проекций), но можно использовать CQRS без event sourcing и наоборот.
Зачем нужны снапшоты?
Чтобы не проигрывать весь журнал при каждом чтении. Снапшот — сохранённое состояние агрегата на определённой версии; при чтении берут последний снапшот и доигрывают только события после него. Это ускоряет восстановление состояния у агрегатов с большим числом событий.
Что делать, если изменилась структура события?
События версионируют и пишут upcaster'ы — функции, которые при чтении преобразуют старый формат события в новый. Удалять или переписывать старые события нельзя: журнал неизменяем. Так система продолжает читать историю после эволюции схемы.
Это официальная информация?
Нет. Статья основана на работах Greg Young и Vaughn Vernon по event sourcing/DDD и типичных вопросах собеседований системного аналитика. Конкретные требования зависят от компании и архитектуры.
Тренируйте системный анализ — откройте тренажёр с 1500+ вопросами для собесов.