Когда CQRS избыточен
CQRS покупает структурное разделение за реальную цену: две модели, eventual consistency, инфраструктура projection. Оправдан только там, где асимметрия записи и чтения реальна. Большинство CRUD-экранов не подходит. CQRS и event sourcing — независимые паттерны.
После того как команда биллинговой платформы внедрила CQRS для домена заказов, паттерн стал стандартным ответом команды. Модуль управления аккаунтами нуждался в экране редактирования данных компании — название, адрес, контакт по выставлению счетов. Разработчик предложил разбить на write model CompanyAggregate и read model CompanyProfileView с async projection. Технический лид возразил: «Какой инвариант охраняет write model? Какая асимметрия записи и чтения оправдывает projection?» Разработчик задумался. Экран редактирования компании не имел сложных инвариантов. У компании было название, адрес и контакт по выставлению счетов — три поля, одна таблица, одна форма. Записям и чтениям нужны были в точности одни и те же данные. Не было никаких проблем с производительностью при чтении нормализованной строки напрямую. Projection добавила бы асинхронный worker, доменное событие, отдельную таблицу read model и eventual consistency — для экрана, который пользователь редактирует раз в месяц. Вердикт технического лида: это CRUD. CQRS здесь — случайная сложность, а не структурная ясность. У той же команды была противоположная проблема в рабочем процессе одобрения заказов: там не было CQRS, была одна нормализованная модель Order на всё, и дашборд одобрения работал мучительно медленно из-за join по семи таблицам для каждой строки. Правильный ответ — не «всегда CQRS» и не «никогда CQRS». Это распознавание тех частей системы, где асимметрия записи и чтения реальна, и применение паттерна только там.
Реальная стоимость CQRS
CQRS не бесплатен. Прежде чем применять его, команда должна чётко понимать, что он вводит:
Две модели для проектирования, построения и поддержки. Каждое изменение write-стороны должно быть распространено на одну или несколько read models. Каждая новая read model — это новая projection для написания, тестирования и эксплуатации. Каждое изменение write-схемы должно оцениваться на предмет влияния на каждую projection.
Eventual consistency там, где её не было. Async projection означает, что read model может устареть. Это вопрос корректности, о котором нужно явно сообщать, управлять и проектировать. Пользователи видят не самые актуальные данные. Некоторые queries, которые раньше были строго согласованными, теперь eventually consistent.
Инфраструктура projection. Async projection требует механизма наблюдения за изменениями write-стороны: message bus с доменными событиями, change-data capture или паттерн outbox. Каждый из них имеет операционные накладные расходы — развёртывания, мониторинг, обработку сбоев, очереди недоставленных сообщений.
Более сложная отладка. Когда пользователь спрашивает «почему мой дашборд показывает неверный статус», ответ может затрагивать write model, pipeline projection, read model и окно eventual consistency. Цепочка причинности длиннее и сложнее для трассировки.
Эти затраты стоят того, когда структурная выгода реальна. Они — потери, когда применяются к проблемам, не имеющим подлинной асимметрии записи и чтения.
Большинство CRUD не требует CQRS
Значительная часть экранов приложений симметрична: те же поля, записанные в базу данных, отображаются обратно. Форма адреса записывает название, улицу, город и почтовый индекс. Экран редактирования читает те же четыре поля. Экран списка показывает те же поля с фильтром. Никакой инвариант поля адреса не охраняет. Никакой сложный join не нужен. Никакой структурной несовместимости записи и чтения не существует.
CQRS для этого экрана добавляет две модели, projection и eventual consistency за ноль структурной выгоды. Write model была бы CompanyAddress. Read model — CompanyAddressView. Projection копировала бы четыре поля из write-таблицы в read-таблицу. Это не архитектура — это церемония.
Эвристика: если данные, которые экран записывает, и данные, которые он читает, имеют одинаковую форму и одинаковые поля, CQRS добавляет сложность без структурной выгоды.
Для биллинговой платформы:
- Редактирование профиля компании (название, адрес, контакт) — CRUD, без CQRS
- Настройки аккаунта пользователя (email, часовой пояс, уведомления) — CRUD, без CQRS
- Дашборд одобрения заказов (join по семи таблицам, агрегация статусов, флаги просрочки) — реальная асимметрия, CQRS оправдан
- Подача заказа (инвариант кредитного лимита, инварианты позиций, переходы статусов) — сложные write-инварианты, CQRS оправдан
▸Why this works
Вопрос, который нужно задать перед применением CQRS, не «можем ли мы разделить это на commands и queries?» Ответ на него всегда да — любой домен можно разделить. Вопрос: «имеют ли забота о записях и забота о чтениях структурно несовместимые требования, оправдывающие поддержку двух отдельных моделей?» Если ответ да — CQRS разрешает реальное структурное противоречие. Если нет — CQRS это архитектурное позолачение.
Частичный и локальный CQRS
CQRS не обязан применяться ко всему приложению. Правильная область — bounded context или доменная область, где асимметрия записи и чтения действительно существует.
На биллинговой платформе домен управления заказами имеет реальную асимметрию: write-сторона имеет сложные инварианты (кредитный лимит, рабочий процесс одобрения, валидация позиций), а read-сторона имеет сложные queries (дашборд, пайплайн менеджера, финансовый отчёт). CQRS там оправдан.
Домен управления аккаунтами по большей части CRUD. Применение CQRS к управлению аккаунтами ради «согласованности с доменом заказов» — это карго-культ архитектуры: копирование паттерна без копирования сил, его оправдывающих.
Правильный ответ: применять CQRS локально к домену управления заказами. Дать домену управления аккаунтами использовать простую нормализованную модель с прямыми чтениями. Двум доменам не нужно быть архитектурно согласованными друг с другом — им нужно соответствовать своим собственным силам.
Это называется локальный CQRS или частичный CQRS: CQRS применяется избирательно там, где присутствует структурная выгода, а не равномерно по всей системе.
CQRS — это не event sourcing
Самое распространённое заблуждение о CQRS — путать его с event sourcing. Это независимые паттерны, которые хорошо дополняют друг друга. Их путаница удваивает стоимость внедрения.
CQRS разделяет write model и read model. Write-сторона хранит текущее состояние в нормализованной реляционной таблице. Read-сторона хранит денормализованные views. Projection читает write-таблицу напрямую или наблюдает доменные события.
Event sourcing (рассматривается в юните 08) хранит состояние как append-only последовательность событий, а не как текущее состояние. Текущее состояние вычисляется воспроизведением журнала событий. Event sourcing — это другая стратегия хранения write-стороны, независимая от того, как обслуживаются чтения.
Можно иметь CQRS без event sourcing: write-сторона — нормализованная реляционная таблица, read-сторона — денормализованная проекция этой таблицы. Это наиболее распространённая форма CQRS в production.
Можно иметь event sourcing без CQRS: сервис использует журнал событий как write-хранилище, но обслуживает чтения напрямую из журнала воспроизведением по запросу — без отдельных read models.
Можно иметь оба: write-сторона — журнал событий; read models строятся путём проецирования журнала событий. Эта комбинация мощная, но несёт стоимость обоих паттернов.
▸lesson.inset.note
Команды, впервые изучающие CQRS, часто встречают его в контексте event sourcing и предполагают, что они всегда идут вместе. Затем они решают, что не могут принять CQRS без принятия event sourcing, что поднимает стоимость внедрения до точки, где ни один не принимается. Реальность: CQRS с реляционной write model и projection, читающей write-таблицу, — это полностью валидная, малозатратная стартовая точка. Event sourcing — это необязательное и значительное дополнительное обязательство.
Команда строит систему с тремя доменами: Order (сложный рабочий процесс одобрения, инварианты кредитного лимита, multi-table дашборд), Product Catalog (название, описание, цена, изображение — простой CRUD, отображается как хранится), и Notifications (пользовательские предпочтения для email/SMS, переключаются на странице настроек). Для каких доменов следует использовать CQRS?
Команда хочет принять CQRS для домена заказов. Инженер говорит: «Мы не можем принять CQRS без event sourcing — они идут вместе». Верно ли это?
Спустя шесть месяцев после применения CQRS к домену заказов, команда обнаружила, что новый инженер потратил неделю на отладку бага, при котором дашборд заказов показывал неверный статус. Корневая причина: в projection был баг, и для некоторых заказов read model содержала устаревшие данные. Технический лид спрашивает: «Был ли CQRS неправильным выбором?» Как следует это оценивать?
- 01Каковы основные затраты, которые вводит CQRS, и когда они окупаются?
- 02Что такое частичный/локальный CQRS и почему он предпочтительнее равномерного применения CQRS?
- 03CQRS и event sourcing — это один паттерн? Можно ли иметь один без другого?
В вступительной истории была правильная интуиция — технический лид задал правильные вопросы: какой инвариант охраняет write model, и какая асимметрия оправдывает projection? Для экрана редактирования адреса компании ответами были «никакой» и «никакой». CQRS там был церемонией, а не архитектурой.
Затраты CQRS реальны: две модели, eventual consistency, инфраструктура projection и более длинные цепочки отладки. Эти затраты покупают реальную структурную выгоду — чистую write model, охраняющую инварианты, read model, сформированную именно для каждого query, независимую развиваемость обеих сторон. Компромисс оправдан там, где асимметрия реальна.
Применяй CQRS локально к доменным областям, где он оправдан. Пусть другие домены остаются на простых нормализованных моделях. Архитектурные паттерны должны следовать структурным силам, а не применяться ради согласованности.
CQRS и event sourcing независимы. Сегодня можно применить CQRS с реляционной write model без обязательства к журналу событий. Event sourcing (юнит 08) — это отдельное, значительное архитектурное решение о стратегии хранения write-стороны, дополняющее CQRS, но не требуемое им.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.