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