CQRS на собеседовании системного аналитика
event_date_text хранит дату как текст в формате YYYY-MM-DD. Вы хотите фильтровать строки за прошлую неделю и группировать по неделе. Что логичнее всего сделать в запросе?Содержание:
Что такое CQRS
CQRS расшифровывается как Command Query Responsibility Segregation — «разделение ответственности между командами и запросами». Термин ввёл Грег Янг около 2010 года, развивая более старый принцип Command-Query Separation Бертрана Мейера. Идея простая: операции, которые меняют состояние системы, и операции, которые читают состояние, разводятся по разным моделям.
В классической архитектуре одна и та же модель данных обслуживает и запись, и чтение: те же таблицы, те же сущности, тот же ORM. Пока нагрузка небольшая, это удобно. Но когда чтений на порядки больше, чем записей, а бизнес-логика записи становится сложной, единая модель начинает мешать: её невозможно оптимизировать одновременно и под строгую валидацию при записи, и под быстрые агрегированные выборки при чтении. CQRS решает это, позволяя развивать write model и read model независимо — с разными схемами, разными хранилищами и разной степенью нормализации.
На собесе системного аналитика CQRS всплывает в разделе про архитектурные паттерны и часто идёт в связке с event sourcing и DDD. От вас ждут не пересказ определения, а понимание trade-offs: что вы выигрываете, чем платите и когда паттерн оправдан.
Команды и запросы
Фундамент CQRS — строгое различие между командой и запросом. Каждая операция должна быть либо одним, либо другим, но не тем и другим сразу.
Команда (command) изменяет состояние системы и не возвращает данные — максимум подтверждение или идентификатор. Команда выражает намерение пользователя в терминах предметной области: не «UPDATE строки в таблице», а «оформить заказ», «отменить бронь», «подтвердить платёж».
PlaceOrder(items, customer_id) → void / OrderIdЗапрос (query) возвращает данные и не меняет состояние. Запрос идемпотентен: сколько раз ни вызови — система остаётся в том же состоянии.
GetOrder(order_id) → OrderDTOСмысл строгого разделения в том, что команды и запросы имеют разные требования. Команда проходит через валидацию, бизнес-правила, проверку инвариантов — здесь важна консистентность. Запрос должен быть быстрым и часто отдаёт данные в форме, удобной конкретному экрану. Смешивать их в одном методе означает мешать этим требованиям работать. Классический анти-паттерн — метод, который «заодно» что-то записывает при чтении (например, инкрементит счётчик просмотров прямо в геттере): такой метод перестаёт быть предсказуемым.
Раздельные модели чтения и записи
Из разделения операций вытекает главная особенность CQRS — две отдельные модели данных.
Write model (модель записи) оптимизирована под консистентность и валидацию. Обычно нормализована, содержит богатые доменные сущности с бизнес-логикой (rich domain model), защищает инварианты предметной области. Её задача — не дать системе перейти в некорректное состояние.
Read model (модель чтения) оптимизирована под конкретные сценарии чтения. Часто денормализована, чтобы отдавать данные без джойнов, и существует в нескольких вариантах — по одному под каждый тип экрана или отчёта. Она ничего не валидирует, её задача — отдать готовые данные максимально быстро.
Write side: PostgreSQL, нормализованные таблицы — orders, items, payments.
Read side:
- OrderSummary — денормализованная карточка заказа, кешируется.
- CustomerHistory — история покупок клиента, агрегат.
- RevenueDashboard — предагрегированная выручка по дням.Важный нюанс, который любят проверять на собесе: read и write модели не обязаны жить в одной СУБД. Запись может идти в PostgreSQL, а чтение — из Elasticsearch, Redis или ClickHouse, куда данные проецируются в удобном виде. Именно эта свобода делает CQRS мощным для read-heavy систем: каждую сторону можно масштабировать и оптимизировать отдельно.
Синхронизация моделей
Раз моделей две, встаёт вопрос: как read model узнаёт об изменениях в write model. Обычно это делают через события и проекции.
Схема потока: команда меняет write model → система публикует событие → подписчики (проекции) обновляют соответствующие read model.
Заказ сохранён → публикуется событие OrderPlaced.
Подписчики обновляют модели чтения:
- проекция OrderSummary,
- проекция CustomerHistory,
- аналитический агрегат.Ключевое следствие — eventual consistency (согласованность в конечном счёте). Read model отстаёт от write model на некоторый лаг: обычно от миллисекунд до секунд, в зависимости от того, синхронно или асинхронно обновляются проекции. Это значит, что сразу после успешной команды пользователь может ещё не увидеть изменение в списке — данные «догонят» чуть позже.
На собесе обязательно спросят, как жить с этим лагом. Хорошие ответы: показывать пользователю его собственное изменение оптимистично на клиенте, не дожидаясь проекции; для критичных сценариев читать из write model напрямую; проектировать UX так, чтобы небольшая задержка была допустима. Плохой ответ — делать вид, что система строго консистентна: если вы выбрали CQRS с асинхронными проекциями, вы сознательно приняли eventual consistency, и это надо уметь объяснить бизнесу.
Когда применять
CQRS — не архитектура по умолчанию, а точечный инструмент. На собесе за умение сказать «здесь он не нужен» ценят не меньше, чем за знание механики.
Паттерн оправдан, когда:
- Чтений кратно больше, чем записей, и их профили нагрузки расходятся — read и write нужно масштабировать по-разному.
- Одни и те же данные нужны в нескольких разных представлениях (карточка, список, дашборд, экспорт), и единая модель под все не оптимальна ни для одного.
- Домен сложный, с богатой бизнес-логикой на записи — и CQRS хорошо ложится в связку с DDD и event sourcing.
- Производительность чтения критична, а денормализованные проекции дают заметный выигрыш.
Паттерн избыточен, когда:
- Это простой CRUD без сложной логики — разделение только добавит кода и точек отказа.
- Нет проблем с масштабированием — вы решаете несуществующую проблему.
- Команда маленькая и без опыта с CQRS: eventual consistency, проекции и дублирование данных повышают сложность эксплуатации.
Главный тезис для собеса: CQRS добавляет сложность (две модели, синхронизация, лаг консистентности), поэтому применять его надо выборочно и осознанно, а не «потому что модно». Часто разумный компромисс — применить CQRS только к тем частям системы, где выигрыш реален, оставив остальное на обычном CRUD.
Частые ошибки
Путать CQRS с event sourcing. Это разные паттерны, которые часто идут вместе, но не требуют друг друга. CQRS — про разделение моделей чтения и записи. Event sourcing — про хранение состояния как последовательности событий. Можно делать CQRS без event sourcing и наоборот.
Считать, что CQRS обязательно означает две базы данных. Разделение может быть логическим: те же таблицы, но разные модели/сервисы для чтения и записи. Отдельное хранилище под чтение — частый, но не обязательный шаг.
Игнорировать eventual consistency в UX. Если проекции обновляются асинхронно, интерфейс должен это учитывать. Кандидаты, которые обещают «строгую консистентность» при асинхронных проекциях, противоречат сами себе.
Тащить CQRS в простой CRUD. Самая частая ошибка на практике — усложнение там, где хватило бы одной таблицы. На собесе это проверяют вопросом «а когда НЕ надо».
Связанные темы
- DDD для SA
- Event-driven архитектура для SA
- Архитектурные паттерны для SA
- Domain events для SA
- Подготовка к собесу системного аналитика
FAQ
Чем CQRS отличается от event sourcing?
Это ортогональные паттерны. CQRS разделяет модели чтения и записи. Event sourcing хранит состояние как поток событий, из которых оно восстанавливается. Их часто применяют вместе (события удобно проецировать в read model), но каждый работает и по отдельности: CQRS можно построить на обычных таблицах, а event sourcing — без разделения на две модели.
Обязательно ли для CQRS иметь две базы данных?
Нет. Разделение может быть чисто логическим — разные модели и сервисы поверх одной базы. Отдельное хранилище под чтение (например, Elasticsearch или Redis) добавляют, когда профили нагрузки чтения и записи действительно расходятся и это оправдывает эксплуатационную сложность.
Что такое eventual consistency в контексте CQRS?
Это согласованность в конечном счёте: read model обновляется не мгновенно, а с задержкой после изменения write model — обычно от миллисекунд до секунд. Сразу после команды пользователь может ещё не увидеть результат в списках и отчётах. С этим борются оптимистичным обновлением на клиенте, чтением критичных данных из write model напрямую и продуманным UX.
Когда CQRS избыточен?
Для простого CRUD без сложной бизнес-логики, при отсутствии проблем с масштабированием и в маленькой команде без опыта эксплуатации распределённых систем. В этих случаях две модели и синхронизация добавляют больше сложности, чем пользы. Часто правильнее применить CQRS точечно — только к нагруженным или сложным частям системы.
Это официальная информация?
Нет. Статья основана на работах Грега Янга и Мартина Фаулера, а также на общей практике проектирования систем. Конкретные формулировки вопросов зависят от компании, команды и уровня позиции.
Тренируйте системный анализ — откройте тренажёр с 1500+ вопросами для собесов.