open atlas
↑ К треку
Архитектурные паттерны ARCH · 08 · 01

События как источник истины

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

ARCH Senior ◷ 26 min
Уровень
ОсновыJuniorMiddleSenior

Биллинговая команда получила звонок от финансового директора в 21:00: «Скажите точно, в каком состоянии был заказ №48291 третьего марта в 14:00 и почему он изменился с “ожидает” на “одобрен” тем днём». Команда открыла базу данных. В таблице orders была одна строка для заказа №48291 с текущим статусом: «выставлен счёт». История исчезла. Таблица хранила только последнее состояние. Никакой записи о том, когда заказ был одобрен, кто это сделал, какими были позиции в момент одобрения и почему статус изменился, — не было. Историю восстанавливали по логам приложения — ненадёжным, неполным, не синхронизированным с деплоями. Чтобы дать частичный ответ, ушло шесть часов. Финансовый директор остался недоволен. Эта команда построила абсолютно нормальную CRUD-систему. Каждый UPDATE orders SET status = 'approved' уничтожал предыдущее состояние. «Почему» — бизнес-событие — нигде в схеме не было. Им нужен был не лучший запрос и не лучший индекс. Им нужна была другая модель того, что хранить в принципе.

CRUD-модель уничтожает историю

Обычная CRUD-система хранит текущее состояние. Когда заказ одобряется, приложение выполняет UPDATE orders SET status = 'approved' WHERE id = 48291. Предыдущее состояние — «ожидает» — исчезло. В базе данных нет никакой записи о переходе: ни когда он произошёл, ни что его вызвало, ни каким был payload в момент принятия решения.

Это не баг приложения. Это следствие модели данных. Реляционные таблицы спроектированы для хранения последнего снимка сущности. Такая модель эффективна, привычна и уместна для большого класса задач. Но у неё есть структурное следствие: история того, как сущность пришла к текущему состоянию, по умолчанию отбрасывается.

Команда, восстанавливавшая историю для финансового директора, делала вручную то, что должна была автоматически сохранить модель данных. Они обратно инженерили историю из логов, временных меток коммитов и памяти.

Модель event sourcing: только добавление, никогда перезапись

Event sourcing инвертирует это. Вместо хранения текущего состояния заказа вы храните каждое бизнес-событие, которое когда-либо произошло с этим заказом — в append-only журнале. Строки в таблице событий для заказа №48291 выглядят так:

seq  event_type           payload                          occurred_at
1    OrderPlaced          {customer_id, line_items}        2024-03-01 09:12
2    OrderReviewStarted   {reviewer_id}                    2024-03-02 14:33
3    OrderApproved        {approved_by, reason}            2024-03-03 14:07
4    InvoiceGenerated     {invoice_id, amount}             2024-03-04 11:55

Эти строки никогда не обновляются, никогда не удаляются. Событие, которое произошло, — это факт в прошедшем времени. OrderPlaced — это не описание текущего состояния, это запись о том, что размещение произошло. Event store — структурное выражение этой неизменяемости.

Текущее состояние — это left-fold журнала событий

Если журнал событий — источник истины, как получить текущее состояние заказа? Нужно свернуть (reduce) события слева направо, применяя каждое событие к аккумулятору, начинающемуся с пустого состояния:

currentState = events.reduce(applyEvent, initialState)

applyEvent — чистая функция: принимает текущее накопленное состояние и событие, возвращает следующее состояние. Никаких побочных эффектов. Для каждого типа события она знает, как продвинуть состояние:

applyEvent(state, OrderPlaced)         → state со status: "pending", line_items: [...]
applyEvent(state, OrderReviewStarted)  → state со status: "under_review", reviewer_id: ...
applyEvent(state, OrderApproved)       → state со status: "approved", approved_by: ...
applyEvent(state, InvoiceGenerated)    → state со status: "invoiced", invoice_id: ...

Результат свёртки — текущее состояние агрегата. Оно производно, а не хранится. Когда приложению нужно обработать команду для заказа №48291, оно загружает журнал событий этого заказа и сворачивает его, восстанавливая текущее состояние. Состояние никогда не записывается в нормализованный столбец.

lesson.inset.note

Это структурная связь между event sourcing и CQRS (юнит 07). Event sourcing касается write-стороны — как хранится состояние агрегата. CQRS — отдельный паттерн, разделяющий read и write модели. Они хорошо компонуются, но независимы: можно использовать event sourcing без CQRS и CQRS без event sourcing. Урок по проекциям CQRS (юнит 07) упоминал replay из журнала событий — это работа event sourcing.

События фиксируют намерение, а не только итоговый снимок

Критическое свойство хорошо названных событий: они фиксируют что произошло на уровне бизнеса, а не только результирующее изменение данных. OrderApproved — это не то же самое, что «столбец status изменился с under_review на approved». Название несёт намерение: было принято бизнес-решение одобрить заказ. Payload несёт контекст: кто одобрил, код причины, действующий порог одобрения на тот момент.

Это различие важно, когда доменная логика сложна. Рассмотрим изменение количества в позиции:

  • CRUD: UPDATE order_items SET quantity = 5 WHERE id = 77 — только итоговый снимок
  • Event sourcing: LineItemQuantityAdjusted { item_id: 77, previous_qty: 3, new_qty: 5, adjusted_by: "ops-team", reason: "supplier_substitution" } — намерение + контекст

Через шесть месяцев, когда возникнет спор о том, почему изменилось количество, event-sourced система ответит точно. CRUD-система предложит только цифру 5.

Викторина

Команда обсуждает, применять ли event sourcing для агрегата заказов биллинговой платформы. Разработчик аргументирует: «Историю аудита можно получить, включив отслеживание изменений на уровне базы данных (CDC) или ведя отдельную таблицу аудита. Нам не нужно менять всю модель данных». В чём структурное различие между CDC/audit-log подходами и настоящим event sourcing?

Суперсила аудита и временны́х запросов

Структурное следствие хранения журнала событий как источника истины: вопрос финансового директора — «в каком состоянии был заказ №48291 третьего марта в 14:00?» — становится тривиальным:

events.filter(e => e.occurred_at <= "2024-03-03T14:00").reduce(applyEvent, initialState)

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

Временны́е запросы — не болт-он фича в event-sourced системах. Это структурное следствие модели данных. Журнал событий сохраняет каждый переход состояния; свёртка его префикса даёт любое прошлое состояние. Это делает event sourcing естественным выбором для доменов, где аудит и временны́е запросы — требования первого класса: финансовые системы, регулируемые домены, логистика, медицинские записи.

Викторина

Команда проектирует агрегат заказов для event-sourced системы. Разработчик предлагает назвать событие 'OrderStatusUpdated' с payload { new_status: 'approved' }. Старший инженер возражает. В чём возражение и почему оно важно?

Чего event sourcing не является: замена CRUD повсюду

Event sourcing влечёт структурные издержки. Журнал событий для активно мутирующего агрегата накапливается бесконечно. Загрузка агрегата с тысячами событий требует их свёртки — производительность деградирует без митигации (снапшоты, рассматриваются в уроке 2 юнита 08). Запросы к текущему состоянию множества агрегатов одновременно требуют read model (проекции), построенной из журнала событий, — то есть запуска и поддержки pipeline проекций. Write-сторона и read-сторона теперь отдельные системы.

Это не теоретические издержки. Команды, применяющие event sourcing ко всем сущностям в системе — включая те, у которых нет требований к аудиту или временны́м запросам, — накапливают эти издержки без выгоды. Event sourcing подходит для агрегатов, где:

  • История переходов состояния — бизнес-требование первого класса (аудит, комплаенс, урегулирование споров)
  • Нужны временны́е запросы («на дату X»)
  • Намерение за каждым изменением состояния нужно сохранять как структурированные данные
  • Доменная логика достаточно сложна, чтобы журнал событий служил полным, инспектируемым протоколом бизнес-решений

Для сущности UserPreference, хранящей состояние переключателя UI, event sourcing — почти наверняка оверкилл. Знание того, что пользователь двадцать раз переключил тёмную тему, не является бизнес-требованием.

Викторина

Биллинговая команда загружает агрегат заказа для обработки команды. У заказа 4200 событий в журнале. Текущая реализация сворачивает все 4200 событий при каждой команде. В чём структурная проблема и каково стандартное её решение в event sourcing?

Вспомните перед уходом
  1. 01
    Что значит, что текущее состояние — это 'left-fold журнала событий'?
  2. 02
    Почему события в event sourcing используют названия в прошедшем времени (OrderApproved), а не в повелительном (ApproveOrder)?
  3. 03
    В чём ключевое структурное различие между event sourcing и добавлением таблицы аудита к CRUD-системе?
Итог

Звонок финансового директора в 21:00 обнажил структурную цену CRUD-модели: каждый UPDATE уничтожал предыдущее состояние. История того, как заказ пришёл к текущему статусу, исчезла. Её реконструкция из логов заняла шесть часов и дала частичный ответ.

Event sourcing решает это структурно, а не как запоздалую доработку. Журнал событий — append-only последовательность неизменяемых бизнес-фактов в прошедшем времени — является источником истины. Текущее состояние вычисляется свёрткой журнала, а не хранится напрямую. Свёртка детерминирована и воспроизводима. Временны́е запросы бесплатны: отфильтруй журнал до нужной временно́й метки и сверни префикс.

Хорошо названные события несут намерение. OrderApproved с одобряющим и кодом причины — это не то же самое, что изменение столбца status. Это разница между мутацией данных и бизнес-фактом. Именно это делает журнал событий полным, проверяемым протоколом бизнес-решений, а не просто историей записей строк.

Структурная цена реальна: производительность свёртки деградирует с длинными журналами (решается снапшотами); read-запросы требуют отдельного слоя проекций (рассматривается в уроке 2); эволюция схемы вводит задачи версионирования (рассматриваются в уроке 3). Event sourcing — не правильная модель для каждого агрегата. Это правильная модель, когда история бизнес-решений — требование первого класса.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.