Получить доступ

CQRS и Event Sourcing

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

CQRS: две модели вместо одной

CQRS (Command Query Responsibility Segregation) означает буквально одно: модель для записи и модель для чтения — разные. Команды меняют состояние и ничего содержательного не возвращают; запросы возвращают данные и ничего не меняют.

Мотивация практическая. У записи и чтения принципиально разные требования. Записи нужны инварианты, нормализованная схема и небольшие транзакции над агрегатом. Чтению нужны денормализованные представления под конкретные экраны, агрегаты, поиск и совсем другой профиль нагрузки — как правило, на порядок больше запросов. Одна модель, обслуживающая оба сценария, вырождается в компромисс: либо тяжёлые JOIN на каждый экран, либо денормализация, ломающая инварианты записи.

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

Полный CQRS — это отдельное хранилище для чтения, наполняемое асинхронно событиями записи. Вот здесь и появляется главная плата: read model отстаёт. Пользователь, который только что нажал «сохранить», может не увидеть изменение в списке. Это не баг, а свойство схемы, и его надо явно закладывать в UI — оптимистичным обновлением на клиенте, чтением своих же записей из модели записи или явной индикацией обработки.

Частое заблуждение: CQRS не требует event sourcing, а event sourcing не требует CQRS. Их регулярно применяют вместе, потому что они хорошо стыкуются, но это независимые решения.

Event Sourcing: источником истины становится лог

Обычное приложение хранит текущее состояние и затирает предыдущее: после UPDATE прежнего значения нет. Event sourcing переворачивает это — хранится неизменяемая последовательность произошедших событий (OrderPlaced, ItemAdded, OrderPaid), а текущее состояние получается их последовательным применением.

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

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

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

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

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

В связке event sourcing и CQRS модель чтения строится проекциями: подписчик читает поток событий и поддерживает денормализованное представление под конкретный сценарий — список заказов пользователя, витрину товара, поисковый индекс.

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

Практические требования к проекциям, которые отличают знающий ответ. Обработка обязана быть идемпотентной, потому что доставка at-least-once и события будут повторяться. Порядок гарантирован только внутри потока одного агрегата (в Kafka — внутри партиции), поэтому ключ партиционирования обычно совпадает с идентификатором агрегата. И нужно наблюдать за отставанием проекции — lag становится важной эксплуатационной метрикой, влияющей на пользователя.

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

Когда этого делать не надо

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

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

Где применение оправдано: там, где история и аудит — часть бизнес-требований (финансы, склад, страхование, комплаенс), где важна реконструкция «как мы пришли в это состояние», где сильно расходятся профили чтения и записи, и там, где несколько разных представлений одних и тех же данных нужны разным потребителям.

Разумная стратегия — применять по нарастающей: сначала разделить команды и запросы внутри приложения (дёшево и почти всегда полезно), затем при необходимости завести отдельную read model, и только для контекстов с реальной потребностью в истории переходить к event sourcing.

Как отвечать на собеседовании

Первым делом разведите CQRS и event sourcing: это независимые решения, которые часто, но не обязательно применяются вместе. Уже одно это отсекает половину заученных ответов.

По CQRS обязательно назовите два уровня — простое разделение команд и запросов внутри приложения (дёшево, безопасно) и отдельное хранилище для чтения (появляется отставание read model). И скажите, как отставание закладывают в продукт, а не как с ним борются.

По event sourcing после определения сразу переходите к практике: снапшоты против дорогого восстановления, оптимистичная конкуренция по версии потока, версионирование и апкастинг событий, идемпотентность проекций и мониторинг их отставания. Про крипто-шреддинг для удаления персональных данных упомяните, если разговор зайдёт о регуляторике.

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

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем