Event Sourcing на собеседовании системного аналитика

Проверь себя · 1/3разбор после ответа
В таблице платежей для каждой транзакции нужен накопительный итог сумм пользователя на этот момент. Какое выражение даёт корректный результат?

Идея 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 — мощнейшее свойство подхода, но на больших журналах он медленный (миллионы событий надо перечитать), поэтому его комбинируют со снапшотами и запускают на реплике, чтобы не мешать проду.

Готовишься к собесу системного аналитика?
827 вопросов: REST, UML, OAuth, ERD, требования. Тренируйся в Telegram
Тренировать SA в Telegram

Когда применять

Подходит, когда:

  • Критичен аудит. Банкинг, финансы, комплаенс — нужна неизменяемая история всех изменений.
  • Нужны запросы «на момент времени». «Каким было состояние заказа на 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. Для справочников и настроек журнал событий — избыточная сложность без выгоды.

Связанные темы

FAQ

Чем event sourcing отличается от обычного хранения состояния?

Обычная модель хранит текущий снимок (строку с полями) и перезаписывает его при каждом изменении — история теряется. Event sourcing хранит неизменяемый журнал событий-фактов, а текущее состояние вычисляет их проигрыванием. Это даёт полный аудит и возможность узнать состояние на любой момент прошлого.

Event sourcing и CQRS — это одно и то же?

Нет. CQRS разделяет модель записи и модель чтения. Event sourcing — способ хранить данные в виде журнала событий. Их часто применяют вместе (запись в журнал, чтение из проекций), но можно использовать CQRS без event sourcing и наоборот.

Зачем нужны снапшоты?

Чтобы не проигрывать весь журнал при каждом чтении. Снапшот — сохранённое состояние агрегата на определённой версии; при чтении берут последний снапшот и доигрывают только события после него. Это ускоряет восстановление состояния у агрегатов с большим числом событий.

Что делать, если изменилась структура события?

События версионируют и пишут upcaster'ы — функции, которые при чтении преобразуют старый формат события в новый. Удалять или переписывать старые события нельзя: журнал неизменяем. Так система продолжает читать историю после эволюции схемы.

Это официальная информация?

Нет. Статья основана на работах Greg Young и Vaughn Vernon по event sourcing/DDD и типичных вопросах собеседований системного аналитика. Конкретные требования зависят от компании и архитектуры.


Тренируйте системный анализ — откройте тренажёр с 1500+ вопросами для собесов.