CQRS на собеседовании системного аналитика

Проверь себя · 1/3разбор после ответа
В таблице событий поле 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, и это надо уметь объяснить бизнесу.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Когда применять

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. Самая частая ошибка на практике — усложнение там, где хватило бы одной таблицы. На собесе это проверяют вопросом «а когда НЕ надо».

Связанные темы

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+ вопросами для собесов.