CQRS и Event Sourcing не стоит применять, если в системе нет сложных правил изменения данных, нет требования хранить и пересобирать историю или нет людей, которые будут это поддерживать. В остальных случаях почти всегда хватает индексов, представлений в базе, таблицы истории и реплики для отчётов: они решают те же проблемы дешевле и без новых режимов отказа.

Статья не против подхода: у него есть честные применения, о них в предпоследнем разделе. Но в обсуждениях его слишком часто выбирают раньше, чем замерили проблему. Как устроены ступени от простого к сложному и как мы их внедряем, описано на странице «Внедрение CQRS и Event Sourcing», здесь разбираем, когда не надо.

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

Что именно вы покупаете

Чтобы взвешивать, нужно понимать цену. Разделение чтения и записи (CQRS) означает две модели данных вместо одной: у каждой свой код, свои тесты, своя схема. Когда чтение строится из отдельного хранилища, между записью и чтением появляется задержка. Пользователь нажал кнопку, обновил страницу и видит старое: это не сбой, а свойство схемы.

Журнал событий (Event Sourcing) добавляет больше. Состояние объекта не хранится, а собирается из ряда фактов. Отсюда четыре последствия: события нужно версионировать, потому что журнал живёт дольше кода; данные для экрана строятся проекциями, которые иногда приходится пересобирать целиком; ошибки в прошлом исправляются новыми событиями, а не правкой; отладка требует собственных инструментов, потому что в базе нет готового «текущего состояния».

Это не недостатки, а цена возможностей. Вопрос в том, нужны ли вам возможности. Историю изменений и состояние на дату можно получить дешевле, а настоящая выгода журнала появляется в узком классе задач.

Шесть признаков, что вам это не нужно

Если вы узнаёте свою систему в строках ниже, начните с дешёвой замены из правого столбца.

ПризнакЧто за этим стоитЧто делать вместо
Система в основном «создать, прочитать, изменить, удалить»Правила тривиальны, модель записи и модель чтения совпадаютОбычная модель с хорошими индексами. Разделение даст только двойной код
Нужна история «кто что менял»Это требование аудита, а не логикиТаблица истории с полями «что, кто, когда, было, стало» и журнал действий
Тормозят несколько отчётовПроблема в запросах или блокировках, а не в архитектуреИндексы, переписанные запросы, материализованные представления, реплика базы для отчётов
Нужны остатки или суммы на датуСрезы на конкретные даты, а не вся историяСнимки состояния по расписанию или записи с датами действия
Команда небольшая и никто не знает подходВерсии, пересборка и инструменты потребуют времени и дисциплиныПростая схема, которую команда сможет сопровождать после сдачи
Пользователи ждут результата сразуЗадержка чтения недопустима в основных сценарияхЕдиная модель в одной базе с транзакциями

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

Простые замены, которые закрывают ту же потребность

Почти у каждого довода в пользу сложного подхода есть дешёвое решение. Ниже порядок, в котором стоит пробовать, начиная с самого простого.

  1. Индексы и разбор запросов. Одна из самых частых причин медленных отчётов — запрос без индекса или выборка всей таблицы. Починка занимает дни, а не месяцы.
  2. Материализованные представления. Готовые результаты тяжёлых запросов, обновляемые по расписанию или после изменений. Данные отстают на период обновления, и для большинства отчётов этого достаточно.
  3. Реплика базы для отчётов. Тяжёлые выборки идут на копию, основная база не страдает. Отставание копии — секунды.
  4. Таблица истории. Для каждой важной сущности записывается изменение с автором и временем. Её видит аудитор, из неё строится экран «история заявки».
  5. Снимки по расписанию. Для остатков и сумм: сохраняем состояние на конец дня и получаем срез на любую дату без пересборки.
  6. Раздельные таблицы чтения в той же базе. Если экраны требуют других форм данных, выделяем таблицы чтения, обновляемые в той же транзакции. Это и есть лёгкий вариант CQRS без задержки.

Дальше стоит идти, только когда предыдущий шаг не помог, и фиксировать это замерами. Тогда следующая ступень объяснима: вы знаете, какую именно проблему она решает.

Цена ошибки: как это выглядит через год

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

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

Вывод не в том, что журнал событий ошибочен. Вывод в том, что цена решения проявляется не на старте, а на второй-третий год. Поэтому вопрос «что мы будем делать с этим, когда автор уйдёт» нужно задавать в начале, а не в конце.

Когда подход оправдан

Честные применения существуют. Они сходятся в одном: на фактах и истории строится логика, а не только отчёт.

СитуацияПочему подходитЧто проверить до начала
Сложная предметная область с большим числом переходовКаждый переход — событие с правилами, состояние выводится из фактов, правила проверяются тестамиЕсть ли эксперты предметной области, готовые записать переходы
Нужно задним числом строить новые срезыПроекцию можно пересобрать по всему журналу, пока исходные факты не потеряныКак часто на самом деле меняются запросы к данным
Чтение сильно превышает запись и нагрузки разныеЧтение масштабируется отдельно от записиЗамеры: какая доля нагрузки приходится на чтение и что именно тормозит
Прошлое нельзя трогатьНеизменяемый журнал фиксирует факты без правокДопустима ли задержка чтения и кто будет сопровождать журнал

Даже в этих случаях стоит начинать с одного объекта, а не со всей системы. Переводим то, где история действительно нужна для логики, а справочники и вспомогательные сущности оставляем обычными таблицами.

Вопросы подрядчику, который предлагает CQRS или Event Sourcing

  1. Что именно вы измерили и какая проблема подтверждена замером?
  2. Почему не подходят индексы, представления, реплика и таблица истории?
  3. Какая задержка между записью и чтением получится и что увидит пользователь в этот момент?
  4. Как будут версионироваться события и кто проверяет совместимость?
  5. Сколько времени займёт полная пересборка проекций и как это измерено?
  6. Как исправляются ошибочные данные в прошлом и кто это делает?
  7. Как поступаем с персональными данными, которые нужно удалить?
  8. Кто на нашей стороне сможет сопровождать систему и какие инструменты вы передадите?
  9. Можно ли остановиться на более простой ступени без переписывания?

Хороший подрядчик ответит на эти вопросы цифрами и признает, что часть задач проще закрыть иначе. Общие вопросы к исполнителю собраны в статье «Как выбрать подрядчика на разработку ПО».

Как проверить до решения: небольшой набор замеров

  • Профиль запросов. Журнал медленных запросов за неделю: какие операции тормозят, чтение или запись, в какие часы.
  • Блокировки. Конкурируют ли отчёты с приёмом данных. Если да, часто помогает реплика.
  • История, которая нужна на самом деле. Спросите аудитора и менеджеров, какие вопросы они задавали за год: «кто согласовал», «что было на дату», «почему изменился статус». Запишите их и подберите самое простое хранение, которое ответит.
  • Допустимая задержка. Для каждого экрана выясните, можно ли показать данные с отставанием в несколько секунд.
  • Состав команды. Кто будет сопровождать систему через два года и умеет ли он работать с версиями событий и пересборкой.

На выходе должен получиться документ, где проблема подтверждена числами, а выбор ступени объяснён. С ним можно идти и к нам, и к другому подрядчику. Если системой уже занимался другой исполнитель, помогает аудит кода и архитектуры.

Что делать, если уже внедрили и тяжело

Такое бывает. Упрощать можно по частям, не выбрасывая всё:

  • Определите объекты, для которых история и пересборка действительно используются. Остальные переведите на обычные таблицы с историей.
  • Где задержка чтения мешает, верните синхронное обновление таблиц чтения в той же транзакции.
  • Введите версии для событий, если их нет, и снимите образцы реальных событий для тестов совместимости.
  • Добавьте снимки состояния, чтобы пересборка укладывалась в приемлемое время, и измерьте её на копии.
  • Передайте команде инструменты: просмотр потока событий по объекту, сравнение проекции с источником, скрипт пересборки.

Переход делаем рядом со старым путём и сравниваем результаты, как при любом переносе боевых данных.

Что делаем мы и с чего начать

Начинаем с обследования: 200 000 ₽ и 2–3 недели за один-два процесса, 350 000 ₽ и 3–4 недели, если затронуто несколько систем. На выходе замеры, рекомендация по ступени (в том числе «оставить как есть, добавить индексы»), вилка бюджета. Обследование вычитается из первого этапа, если продолжаете с нами. Цены по ступеням собраны на странице «Тарифы».

Дальше, если нужно, пилот на одном объекте и перевод остальных, и на любой ступени можно остановиться. Мы не внедряем подход «на всю систему», не дежурим круглосуточно, лицензии ФСТЭК у нас нет, юридических заключений о хранении данных не даём; исходный код и инструкции лежат в вашем репозитории с первого коммита. Подробности: «Внедрение CQRS и Event Sourcing».

Частые вопросы

Когда CQRS не нужен?

Когда система в основном создаёт, читает, изменяет и удаляет записи, нет сложных правил изменения данных, а тормозят единичные отчёты. Тогда помогают индексы, представления и реплика для отчётов, а разделение моделей удваивает код.

Когда Event Sourcing не нужен?

Когда история нужна для просмотра и аудита, а не для логики, когда команда небольшая и не знает подход, когда задержка чтения недопустима. Историю изменений даёт таблица истории, состояние на дату дают снимки.

В чём главный минус Event Sourcing?

Цена проявляется не на старте, а через год-два: версии событий, пересборка проекций, исправление ошибок новыми событиями и отладка без готового состояния. Поэтому его оправдывают только задачи, где история нужна для логики.

Что такое итоговая согласованность и почему о ней спорят?

Это задержка между записью и чтением, когда чтение строится из отдельного хранилища. Пользователь может увидеть старые данные сразу после действия. С этим работают в интерфейсе (показывают результат из ответа на команду), а если задержка недопустима, сценарий остаётся на единой модели.

Можно ли получить историю изменений без журнала событий?

Да. Таблица истории с полями «что, кто, когда, было, стало» закрывает большинство требований аудита. Журнал событий нужен, когда на истории строится логика или нужно задним числом строить новые срезы.

Можно ли начать с простого и перейти позже?

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

Сколько стоит разобраться, нужен ли вам этот подход?

Обследование стоит 200 000 ₽ за один-два процесса и 350 000 ₽ за несколько систем, срок 2–3 и 3–4 недели. Если продолжаете с нами, сумма вычитается из первого этапа. Ответ «не нужно, добавьте индексы» тоже результат обследования.

Как быть с персональными данными в неизменяемом журнале?

Технически: хранить их отдельно и класть в событие ссылку либо шифровать ключом субъекта, который можно уничтожить. Какие данные и на каких основаниях удалять, решает юрист заказчика; мы закладываем возможность.