← назад к разделу

В большинстве приложений мы храним текущее состояние: строка в таблице orders со статусом paid и суммой 1000. Каждое изменение — это UPDATE, который перезатирает прошлое: был статус new, стал paid, и старое значение исчезло навсегда. Обычно этого достаточно. Но иногда важно не только что сейчас, а как мы к этому пришли. Event Sourcing отвечает именно на второй вопрос: вместо текущего состояния он хранит неизменяемый поток событий, а состояние вычисляет из него.

Состояние против потока событий

Представьте банковский счёт. Есть два способа рассказать о нём.

Первый — назвать баланс: «на счёте 1200 рублей». Это состояние. Компактно, но вы не знаете, откуда взялась сумма.

Второй — показать выписку: «+1000 зарплата, −300 продукты, +500 возврат». Это поток событий. Баланс здесь нигде не записан отдельно — он получается сложением строк выписки. И именно выписка первична: банк хранит историю операций, а баланс — производная от неё.

Event Sourcing переносит эту идею в код. Мы не храним «заказ оплачен, сумма 1000». Мы храним факты, которые с заказом происходили, в том порядке, в котором они происходили. А текущий заказ собираем, «проигрывая» эти факты один за другим.

Как выглядит поток событий

События — это факты в прошедшем времени: не команда «оплати заказ», а свершившийся факт «заказ оплачен». Для одного заказа поток может выглядеть так:

OrderCreated     { orderId: 42, customer: "Аня" }
ItemAdded        { orderId: 42, item: "книга", price: 500 }
ItemAdded        { orderId: 42, item: "ручка", price: 100 }
ItemRemoved      { orderId: 42, item: "ручка" }
OrderPaid        { orderId: 42, amount: 500 }

Каждая строка неизменяема: записав событие, мы его больше не трогаем. Если клиент передумал и убрал товар — мы не удаляем ItemAdded, а дописываем новое событие ItemRemoved. История не переписывается, она только растёт. Это ключевое свойство: прошлое нельзя стереть, можно только добавить новый факт.

Хранилище таких событий называется event store. По сути это таблица «только на добавление» (append-only): новые события пишутся в конец, старые никогда не меняются и не удаляются.

Как из событий собирается состояние

Чтобы узнать текущее состояние заказа, мы берём все его события по порядку и применяем одно за другим к пустой заготовке. Этот процесс называется replay (проигрывание):

старт:        заказ пустой
OrderCreated  → заказ 42, покупатель Аня, товаров нет, сумма 0
ItemAdded     → + книга 500     → сумма 500
ItemAdded     → + ручка 100     → сумма 600
ItemRemoved   → − ручка         → сумма 500
OrderPaid     → статус: оплачен, сумма 500
итог:         заказ 42, оплачен, книга, сумма 500

Логика применения одного события к состоянию — это обычная функция: «взять текущее состояние и событие, вернуть новое состояние». Прогнав так весь поток, мы получаем актуальный заказ. Важно, что результат детерминирован: одни и те же события в том же порядке всегда дают одинаковое состояние.

Проекции и связь с CQRS

Проигрывать весь поток каждый раз, когда кто-то открывает список заказов, — дорого и медленно. Поэтому Event Sourcing почти всегда идёт в паре с CQRS — разделением на запись и чтение.

Работает это так:

  • Командная сторона (запись) принимает команду, проверяет правила и дописывает в event store новое событие. Она работает только с потоком фактов.
  • Запросная сторона (чтение) заранее строит удобные для чтения таблицы — проекции (их ещё называют read-model). Проекция подписана на поток событий: пришло OrderPaid — она обновила строку в таблице orders_view, где лежит уже готовое состояние.

Одну и ту же ленту событий можно проигрывать в разные проекции: одну — для списка заказов, другую — для отчёта по выручке, третью — для аналитики. Появилась новая потребность в отчёте — вы пишете новую проекцию и проигрываете в неё всю историю с нуля, не трогая ни командную сторону, ни существующие проекции. Это одна из сильных сторон подхода.

Снапшоты: чтобы не проигрывать всё с нуля

У долгоживущей сущности событий могут быть десятки тысяч. Проигрывать их все, чтобы восстановить состояние, — медленно. Решение — снапшот (snapshot): периодически сохранять «слепок» состояния на определённой позиции в потоке.

Аналогия — та же банковская выписка. Чтобы узнать баланс, вам не нужно складывать операции с открытия счёта: берётся баланс на конец прошлого месяца (снапшот) и к нему прибавляются операции текущего месяца.

Так же и здесь: сохранили снапшот заказа после 1000-го события — и при следующем восстановлении берём снапшот и проигрываем только события после него. Снапшот — это оптимизация, а не источник правды: его всегда можно выкинуть и пересобрать из событий заново.

Плюсы: полный аудит и «машина времени»

Главная ценность в том, что вы никогда ничего не теряете.

  • Полный аудит из коробки. Вопрос «кто, что и когда менял» отвечается сам собой — история изменений и есть ваша модель данных, а не отдельный лог, который забыли вести.
  • «Машина времени». Можно восстановить состояние на любой момент прошлого: просто проиграть события до нужной точки. Удобно для разбора инцидентов — «а как выглядел заказ вчера в 15:00?».
  • Отладка и воспроизведение. Баг проявился в проде? Можно проиграть те же события у себя и увидеть ровно то же состояние.
  • Новые отчёты задним числом. Понадобилась метрика, о которой год назад не думали, — строите новую проекцию и проигрываете всю историю.

Минусы: почему это дорого и нужно редко

Всё это не бесплатно, и честно сказать: Event Sourcing нужен куда реже, чем кажется на волне интереса к нему.

  • Сложность. Разработчику приходится думать не «обнови поле», а «какое событие произошло и как оно применяется». Порог входа выше, отладка непривычнее.
  • Эволюция схемы событий. События хранятся вечно, а формат со временем меняется. Событие OrderPaid пятилетней давности должно проигрываться и сегодня. Приходится версионировать события и уметь читать старые форматы — это постоянная работа.
  • Отложенная согласованность проекций. Между записью события и обновлением read-model проходит время. Пользователь оплатил заказ, а в списке пару секунд ещё «не оплачен». Это eventual consistency, и её надо закладывать в интерфейс, а не бороться с ней.
  • Инфраструктура. Event store, механизм проекций, снапшоты, доставка событий — всё это надо построить и поддерживать. Часто рядом появляется Kafka как транспорт для событий, и вместе с ней — свои заботы о надёжности доставки.

Поэтому Event Sourcing оправдан там, где история — часть требований, а не приятный бонус: финансы и платежи, где аудит обязателен по закону; сложный домен, где важна причинно-следственная цепочка решений; системы, где регулярно нужны разборы «как мы сюда пришли». Для обычного CRUD, где достаточно текущего состояния, привычные UPDATE проще, дешевле и надёжнее — и это нормальный выбор по умолчанию.

Где это применяется

Event Sourcing — не «продвинутый способ хранить данные», а инструмент под конкретную задачу: когда сама история изменений имеет ценность.

  • Платежи и бухгалтерия. Поток операций — естественная модель, а аудит требуется по регламенту. Здесь событийная модель ложится идеально.
  • Сложные бизнес-процессы. Заказы, страховые дела, заявки с длинным жизненным циклом и множеством переходов — когда важно видеть всю цепочку, а не только финал.
  • Системы с требованием «машины времени». Там, где регулярно нужно восстанавливать состояние на прошлый момент или разбирать инциденты по шагам.

Где спотыкаются начинающие:

  • Берут Event Sourcing на весь проект «чтобы по-взрослому», хотя 90% сущностей — обычный CRUD. Событийную модель применяют точечно, к тем частям домена, где история реально нужна, а не ко всему подряд.
  • Путают Event Sourcing и просто отправку событий в очередь. Публиковать OrderPaid в Kafka — это интеграция между сервисами. Event Sourcing — это когда поток событий и есть ваш источник правды, из которого вычисляется состояние. Это разные вещи, хотя события звучат похоже.
  • Забывают про версионирование событий и через год не могут проиграть старый поток, потому что формат события изменился. Совместимость со старыми событиями надо планировать с самого начала.
  • Ждут от read-model мгновенной согласованности и удивляются, почему только что оплаченный заказ секунду висит «неоплаченным». Отложенность проекций — это свойство подхода, а не баг.

Что учить дальше

Event Sourcing почти не живёт в одиночку — начните с распределённых паттернов и CQRS, в паре с которым он обычно и применяется. Транспортом для событий часто выступает Kafka, так что стоит понять, как устроена доставка сообщений. Отложенная согласованность, которая приходит вместе с проекциями, разбирается в разделе системного дизайна, а надёжность доставки и повторной обработки — в статье про таймауты, ретраи и идемпотентность. Наконец, событийная модель тесно связана с предметно-ориентированным проектированием: события — это язык домена, и придумываются они вместе с бизнесом, а не после.