Аудит и откат
Если регулятор требует показать состояние сущности на конкретный момент (а не «как оно было вчера по бэкапу»), только журнал даёт это без потери данных. CQRS с таблицей — нет.
Блог · Инженерия · Паттерны данных
Глубокий разбор · 18 мин чтения
В 2021-м все писали про «раздельное чтение и запись». К 2024-му стало ясно, что CQRS без событийного журнала — просто два слоя, которые разъезжаются. Разбираем, где ES действительно сильнее, а где оба подхода проигрывают банальной транзакции.
Введение в проблему консистентности
Проблема не в том, что CQRS сложен. Проблема в том, что он переносит источник истины в две модели — и между ними появляется окно, в котором данные не согласованы. В высоконагруженных системах это окно превращается в источник 40% инцидентов на данных.
В классическом CQRS команда OrderPlaced пишет в модель записи, а чтение идёт из денормализованного read-model. Если между записью и применением в read-model произойдёт сбой, у вас «заказ создан, но его нет в списке». Классическая CQRS без событийного журнала не умеет откатить read-model до последнего согласованного состояния — у неё просто нет истории.
Event Sourcing решает это иначе: единственный источник истины — это сам журнал событий. Read-model — просто проекция, которую можно пересобрать из журнала с нуля за минуты. Именно это свойство, а не «паттерн» как таковой, делает ES сильнее в продакшене, где данные бьются друг о друга с частотой в тысячи операций в секунду.
Важно понимать: мы не спорим, нужен ли вам read-model. Нужен. Мы спорим о том, что должен быть первичным артефактом — текущее состояние сущности или поток событий, из которого оно выводится.
Сравнение подходов на примере кода
Разберём один и тот же сценарий — отмену заказа — в двух архитектурах. Разница видна не в объёме кода, а в том, что происходит, когда запись падает на 99-й проценте.
Вариант A — CQRS без событийного журнала. Команда CancelOrder меняет статус в таблице orders и отправляет событие в Kafka для обновления read-model. Если Kafka-продюсер упал после коммита в БД, read-model остаётся со старым статусом. У вас нет механизма, который бы «догнал» рассинхрон, кроме ручного скрипта и тикета.
Вариант B — Event Sourcing. Та же команда добавляет в журнал событие OrderCancelled(orderId, ts, reason). Read-model обновляется асинхронно, но при любом рассинхроне её можно пересобрать: rebuild read-model from event #4_820_112. Журнал не теряет события — он append-only, и это фундаментальное отличие от таблицы, где запись «воткнулась» и не откатывается.
Метрики производительности
Выводы для архитекторов
Событийный журнал окупается не тогда, когда вы «хотите DDD», а когда у вас конкретный набор требований, который невозможно закрыть CRUD-моделью.
Если регулятор требует показать состояние сущности на конкретный момент (а не «как оно было вчера по бэкапу»), только журнал даёт это без потери данных. CQRS с таблицей — нет.
Добавили новое поле в read-model? В CQRS пишете миграцию и бэкуете данные. В ES — пересобираете проекцию из журнала за минуты, без риска «потерять» что-то по пути.
Если у вас CRUD-админка, 500 записей в день и нет требований к аудиту — берите обычную БД с транзакциями. ES здесь добавит сложность без пользы. Честно.
Практический чек-лист
1. Нужна ли вам история состояния, а не только текущее? Если да — журнал обязателен. Если нет — обычная БД дешевле и проще.
2. Готовы ли вы к тому, что read-model — это «кэш», а не источник? В ES вы сознательно принимаете, что read-model может отставать на миллисекунды и что её можно пересобрать. Если команда это не принимает — ES будет болеть.
3. Есть ли у вас метрики рассинхрона? Без мониторинга lag между журналом и read-model вы не узнаете о проблеме, пока её не заметит пользователь. SyncOps даёт этот мониторинг из коробки — lag, checkpoint-age, backlog — в OpenMetrics и трейсах каждого сообщения.
Подключить мониторинг
SyncOps отслеживает checkpoint, backlog и рассинхрон между источником и проекцией. Алерт приходит до того, как пользователь заметит, что данные «не те». Подключите первый пайплайн за 30 минут.