Logo
Overview

CQRS: разделяй чтение и запись — паттерн, который меняет всё

May 2, 2026
9 min read

CQRS — это не «модный паттерн». Это признание простой истины: чтение и запись — разные задачи, и не надо их пытаться обслужить одной моделью.

Если вы когда-нибудь писали API, где POST и GET на один ресурс держались за одну ORM-модель — поздравляю, вы знакомы с классическим CRUD. Рано или поздно приходит один и тот же момент: запросы на чтение растут в сложности (джоины, агрегаты, фильтры), запись начинает мешать чтению, а попытки оптимизировать читающий путь ломают пишущий. Иногда это происходит на шестой месяц проекта. Иногда — раньше.

На ревью кто-то бросает фразу «нам нужен CQRS», и половина команды нервно кивает. Про CQRS все слышали, но видели живьём — единицы. Давайте разберёмся: что это, зачем оно, и почему это точно не серебряная пуля.

Если вы ещё не читали обзор Event-Driven Architecture — сделайте это позже, CQRS расцветает именно в событийной среде. Общий контекст про моделирование домена даёт статья о Domain-Driven Design.

Что такое CQRS простыми словами

CQRS (Command Query Responsibility Segregation) — паттерн, при котором операции изменения состояния (commands) и операции чтения (queries) обрабатываются разными моделями, а в сложных случаях — разными сервисами и даже разными хранилищами.

Идею ещё в 1986 году сформулировал Бертран Мейер — принцип CQS (Command-Query Separation): метод либо что-то меняет, либо что-то возвращает, но не одновременно. CQRS берёт ту же идею и поднимает её с уровня методов на уровень архитектуры.

Грубо говоря: если CQS — про правила хорошего тона внутри класса, то CQRS — про разделение всей системы на два независимых конвейера:

  • Write side (команды) — принимает изменения, валидирует бизнес-правила, обновляет состояние.
  • Read side (запросы) — отдаёт данные, оптимизирована под чтение, обычно денормализована и заточена под конкретные UI-сценарии.

В классическом CRUD у вас одна модель Order — и для обновления заказа, и для отображения списка с подсчётом скидок и статистикой по клиенту. В CQRS это две разные модели. Одна знает про инварианты и бизнес-правила. Другая — про то, как удобнее показать данные во вкладке «Заказы».

Чем CQRS отличается от классического CRUD

Чтобы стало нагляднее — посмотрим на схему. Слева классический подход, где одна модель обслуживает всё. Справа CQRS, где write и read разнесены.

100%
graph LR
  subgraph CRUD["Классический CRUD (одна модель на всё)"]
      UI1["UI / API клиент"]
      APP1["Application Layer"]
      MODEL1["Domain Model"]
      DB1[("Реляционная БД")]
      UI1 -->|"POST / GET"| APP1
      APP1 --> MODEL1
      MODEL1 --> DB1
      DB1 --> MODEL1
      MODEL1 --> APP1
      APP1 --> UI1
  end

  subgraph CQRS_["CQRS (write и read разнесены)"]
      UI2["UI / API клиент"]
      CMD["Command Handler"]
      WRITE["Write Model (агрегаты, инварианты)"]
      WDB[("Write DB (нормализованная)")]
      BUS["Event Bus / Sync"]
      READ["Read Model (проекции под UI)"]
      RDB[("Read DB (денормализованная)")]
      QRY["Query Handler"]

      UI2 -->|"Commands"| CMD
      CMD --> WRITE
      WRITE --> WDB
      WDB -.->|"события / репликация"| BUS
      BUS --> READ
      READ --> RDB
      UI2 -->|"Queries"| QRY
      QRY --> RDB
  end

  style UI1 fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style APP1 fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style MODEL1 fill:#f0a500,stroke:#c88400,color:#fff
  style DB1 fill:#7b68ee,stroke:#5a4db2,color:#fff

  style UI2 fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style CMD fill:#50c878,stroke:#3a9a5c,color:#fff
  style WRITE fill:#f0a500,stroke:#c88400,color:#fff
  style WDB fill:#7b68ee,stroke:#5a4db2,color:#fff
  style BUS fill:#e0e0e0,stroke:#999,color:#000
  style READ fill:#50c878,stroke:#3a9a5c,color:#fff
  style RDB fill:#7b68ee,stroke:#5a4db2,color:#fff
  style QRY fill:#50c878,stroke:#3a9a5c,color:#fff

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

Ключевые отличия в виде таблицы

АспектКлассический CRUDCQRS
Модель данныхОдна на запись и чтениеДве независимые модели
База данныхОдна (обычно реляционная)Можно две и больше (PostgreSQL + Elasticsearch, например)
ОптимизацияКомпромисс между OLTP и OLAPКаждая сторона оптимизирована отдельно
СложностьНизкаяСущественно выше
СогласованностьStrong consistency «из коробки»Eventual consistency между write и read
Когда уместно80% типичных бизнес-приложенийСложный домен, неравный профиль нагрузки

Из чего состоит CQRS: команды, запросы и хендлеры

Чтобы CQRS не висел в воздухе как абстракция из чужого учебника, разложим его на конкретные кирпичики.

1. Command (команда)

Команда — это намерение пользователя что-то изменить. Не «отправь HTTP-запрос», а бизнес-намерение в чистом виде:

  • PlaceOrder (оформить заказ)
  • CancelSubscription (отменить подписку)
  • RefundPayment (вернуть платёж)

Команда всегда формулируется в повелительном наклонении и имеет одно состояние ответа: принята или отвергнута. Никаких «верни мне обновлённую сущность» — это не задача команды.

2. Command Handler

Объект, который принимает команду, грузит агрегат из write-хранилища, проверяет инварианты, применяет изменения и сохраняет результат. Если знакомы с DDD — это именно тот слой, где живут агрегаты и доменные сервисы.

3. Query (запрос)

Запрос — это намерение пользователя что-то прочитать. Запросы возвращают DTO, заточенные под конкретный UI-сценарий: «список заказов клиента за месяц с суммой и статусом доставки», «детальная карточка заказа», «дашборд по продажам».

Важно: в CQRS запросы не должны менять состояние. Никаких «прочитал и попутно проставил last_viewed_at». Хотите фиксировать факт просмотра — это отдельная команда MarkOrderAsViewed.

4. Query Handler

Объект, который ходит в read-модель и достаёт нужные данные. Никаких бизнес-правил, никаких агрегатов. Только запрос к базе и маппинг в DTO. Часто это просто SQL или поиск в Elasticsearch.

5. Read Model (проекция)

Самая интересная часть. Read Model не повторяет структуру write-модели — она денормализована и хранит данные в формате, удобном под конкретные запросы.

Например, write-модель хранит Order, Customer, Payment как нормализованные таблицы. А read-модель хранит готовую проекцию OrderListView с уже подтянутым именем клиента, статусом платежа и суммой со скидкой. Никаких джоинов на лету — данные уже в нужной форме.

Чтобы поддерживать read-модель актуальной, write-сторона публикует доменные события, на которые подписаны проекции. Получили OrderCreated — добавили строку в OrderListView. Получили PaymentCompleted — обновили статус. Именно здесь CQRS смыкается с Event-Driven Architecture, и мы плавно переходим к следующей теме.

CQRS и Event Sourcing: почему их часто путают

Эти два паттерна часто упоминают вместе — и из этого рождается путаница. Давайте разберём отдельно.

Event Sourcing — паттерн, при котором текущее состояние системы не хранится напрямую, а восстанавливается из последовательности событий. Вместо «у заказа статус Paid» хранится: OrderCreated → ItemAdded → ItemAdded → PaymentRequested → PaymentCompleted.

CQRS и Event Sourcing — это разные паттерны, но сочетаются они очень хорошо:

  • CQRS можно делать без Event Sourcing — write-модель просто пишет в обычную реляционную БД, а проекции обновляются через триггеры или CDC (Change Data Capture).
  • Event Sourcing можно делать без CQRS — но это редко имеет смысл, потому что восстанавливать сложные read-сценарии из событий по запросу мучительно дорого.

Когда они вместе, картинка становится особенно красивой:

100%
flowchart TD
  USER["Пользователь / API клиент"]
  CMD["Command: PlaceOrder"]
  HANDLER["Command Handler"]
  AGG["Aggregate: Order"]
  ESTORE[("Event Store")]
  EVT1["OrderCreated"]
  EVT2["ItemAdded"]
  EVT3["PaymentCompleted"]
  BUS["Event Bus"]
  PROJ1["Projection: OrderListView"]
  PROJ2["Projection: CustomerStats"]
  PROJ3["Projection: AnalyticsCube"]
  RDB1[("Read DB: PostgreSQL")]
  RDB2[("Read DB: Redis cache")]
  RDB3[("Read DB: ClickHouse")]
  QRY["Query Handler"]
  UIREAD["UI: списки, дашборды, отчёты"]

  USER --> CMD --> HANDLER --> AGG
  AGG --> EVT1 --> ESTORE
  AGG --> EVT2 --> ESTORE
  AGG --> EVT3 --> ESTORE
  ESTORE --> BUS
  BUS --> PROJ1 --> RDB1
  BUS --> PROJ2 --> RDB2
  BUS --> PROJ3 --> RDB3
  UIREAD --> QRY
  QRY --> RDB1
  QRY --> RDB2
  QRY --> RDB3

  style USER fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style CMD fill:#50c878,stroke:#3a9a5c,color:#fff
  style HANDLER fill:#50c878,stroke:#3a9a5c,color:#fff
  style AGG fill:#f0a500,stroke:#c88400,color:#fff
  style ESTORE fill:#7b68ee,stroke:#5a4db2,color:#fff
  style EVT1 fill:#e0e0e0,stroke:#999,color:#000
  style EVT2 fill:#e0e0e0,stroke:#999,color:#000
  style EVT3 fill:#e0e0e0,stroke:#999,color:#000
  style BUS fill:#f0a500,stroke:#c88400,color:#fff
  style PROJ1 fill:#50c878,stroke:#3a9a5c,color:#fff
  style PROJ2 fill:#50c878,stroke:#3a9a5c,color:#fff
  style PROJ3 fill:#50c878,stroke:#3a9a5c,color:#fff
  style RDB1 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style RDB2 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style RDB3 fill:#7b68ee,stroke:#5a4db2,color:#fff
  style QRY fill:#50c878,stroke:#3a9a5c,color:#fff
  style UIREAD fill:#4a90d9,stroke:#2c5f8a,color:#fff

На схеме хорошо видно, почему эти паттерны такие хорошие соседи. Write-сторона пишет события в Event Store, шина разносит их по проекциям, каждая проекция строит свою read-модель в подходящем хранилище. Аналитику в ClickHouse, оперативные списки в PostgreSQL, горячий кеш в Redis — пожалуйста.

Примечательно, что если завтра аналитики попросят новый отчёт — вы просто пишете новую проекцию и прокручиваете все события заново. Read-модель восстанавливается из истории. Ради этого, собственно, люди и связываются с Event Sourcing.

Когда CQRS оправдан, а когда — нет

Самая частая ошибка — внедрить CQRS «потому что красиво». Через полгода команда начинает ненавидеть этот паттерн всей душой, потому что любая мелочь требует обновления двух моделей и трёх проекций. Поэтому честно: CQRS уместен далеко не везде.

Когда CQRS реально окупается

  • Сложный домен с богатыми бизнес-правилами. Если десятки инвариантов на агрегат и сложная валидация — отделение write-модели просто спасает.
  • Сильно асимметричная нагрузка. Например, читаем в 100 раз чаще, чем пишем — типичный e-commerce каталог. Read-модель в таком случае масштабируется независимо.
  • Разные форматы данных под разные UI. Мобильное приложение, веб-кабинет, BI-дашборд — каждому нужна своя проекция. Заставлять одну модель угодить всем дорого.
  • Аудит и история изменений как требование. Вместе с Event Sourcing CQRS решает эту задачу почти бесплатно.
  • Команды работают над write- и read-сторонами параллельно. Деление по моделям становится естественной границей ответственности.

Когда CQRS только навредит

  • Простые CRUD-приложения. Админка, справочник, типовая внутренняя система — два слоя моделей создадут больше проблем, чем решат.
  • Небольшая команда без опыта. CQRS требует дисциплины и понимания eventual consistency. Без этого в итоге получается неподдерживаемое месиво.
  • Жёсткое требование strong consistency. Если бизнес не готов к тому, что после команды read-модель обновится через 50–500 миллисекунд — CQRS уже на старте создаёт проблему.
  • Слабо сформулированный домен. Если вы ещё не уверены, какие bounded contexts у вас есть и что считается агрегатом — сначала разберитесь с этим. И только потом думайте про CQRS. Тут очень помогает DDD простыми словами.

Типичные подводные камни

Допустим, взвесили все «за» и решили: да, нам нужен CQRS. Отлично. Теперь о граблях.

1. Eventual consistency между write и read

Между моментом обработки команды и моментом обновления read-модели проходит время. Иногда миллисекунды, иногда секунды — если консьюмер тормозит. Классический сценарий «нажал кнопку → перешёл на следующую страницу → не вижу изменений» становится нормой.

Решается одним из трёх способов:

  • UX-приёмы: «ваш заказ создан, обновление списка может занять пару секунд».
  • Optimistic UI: на фронте сразу подкладываем ожидаемое состояние, не дожидаясь read-модели.
  • Read-from-write для критичных сценариев: в редких случаях читаем напрямую с write-стороны.

2. Дублирование логики

Без дисциплины быстро получите два места, где «почти одно и то же»: валидаторы на write-стороне и что-то похожее на read-стороне. Дисциплина здесь простая: бизнес-правила живут только на write-стороне. Read-сторона ничего не валидирует — она только показывает.

3. Стоимость инфраструктуры

CQRS — это минимум +1 база данных, +1 шина событий, +1 пайплайн проекций, +1 набор алёртов. Операционная сложность растёт. Команды, которые недооценивают этот пункт, чаще всего и разочаровываются в паттерне потом.

4. Перестройка проекций

Рано или поздно захочется изменить формат read-модели. Есть Event Store — отлично: перепрогнали события и собрали новую проекцию. Нет Event Store — придётся либо мигрировать данные, либо писать сложные backfill-сценарии из write-БД.

5. Соблазн «всё через события»

CQRS работает лучше всего там, где уже есть культура работы с событиями и stateless-сервисами. Внедрять его поверх хаоса из синхронных вызовов и общих БД — больно. Сначала порядок в архитектуре, потом CQRS.

Заключение

CQRS — это не про моду и не про «делать как у Netflix». Это про честное признание факта: операции изменения и операции чтения предъявляют к данным очень разные требования, и одна модель на всё — это всегда компромисс. Иногда терпимый. Иногда — невыносимый.

Если у вас типовое CRUD-приложение и команда из трёх человек — оставьте CQRS в покое. Спокойно живите с одной моделью. Если же домен сложный, нагрузка асимметричная, проекций нужно много, а команда готова принять eventual consistency — CQRS даст ту самую гибкость, ради которой стоит платить операционным усложнением.

И главное правило: не начинайте с CQRS. Начинайте с понятных границ, читаемой write-модели и нормальных запросов. CQRS появится сам — когда тимлид скажет «мы тут собираемся вынести чтение в отдельную базу, иначе всё ляжет». Вот тогда паттерн перестанет быть теорией.

PS. Если вы только что прочитали этот пост и уже готовите речь на завтрашний созвон «давайте перепишем всё на CQRS» — пожалуйста, не надо. Перечитайте раздел «когда CQRS только навредит», заварите чай и подумайте ещё раз. Серьёзно, чай иногда спасает архитектуру лучше любого паттерна.