CQRS паттерн: что это такое и когда применять в разработке

Полное руководство по паттерну CQRS [2026]



CQRS — это архитектурный паттерн, который решает проблему сложных систем, разделяя модели чтения и записи данных.

Мы в Surf проектируем высоконагруженные системы для крупнейших компаний России и Средней Азии — от банковских приложений до e-commerce платформ с миллионами пользователей. Команда из 250+ специалистов знает: правильный выбор архитектуры на старте экономит месяцы разработки и миллионы рублей в будущем.

В этой статье разберём, что такое CQRS, когда его применять, какие преимущества и недостатки он несёт, и как интегрировать этот паттерн в ваш проект.


Содержание

  1. Что такое CQRS
  2. Как работает CQRS
  3. CQRS и Event Sourcing
  4. Когда применять CQRS
  5. Преимущества CQRS
  6. Недостатки и сложности CQRS
  7. Практическая реализация CQRS
  8. CQRS в микросервисной архитектуре
  9. Примеры применения CQRS
  10. Типичные ошибки при внедрении CQRS

Ключевые моменты

Инфографика CQRS

1. Что такое CQRS

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

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

Термин CQRS был введён Грегом Янгом (Greg Young) в 2010 году как развитие принципа CQS (Command Query Separation), сформулированного Бертраном Мейером ещё в 1980-х годах.

Принцип CQS как основа

Прежде чем говорить о CQRS, стоит разобраться с его предшественником. CQS (Command Query Separation) — это принцип проектирования методов, который помогает сделать код предсказуемым. Идея проста: каждый метод в системе должен выполнять ровно одну из двух задач — либо изменять состояние, либо возвращать данные, но никогда оба действия одновременно.

Тип операцииНазначениеВозвращаемое значениеПример
Command (Команда)Изменяет состояние системыvoid (ничего)createOrder(), updateUser()
Query (Запрос)Возвращает данныеДанныеgetOrderById(), listProducts()

Ключевое правило: запросы не должны изменять состояние, а команды не должны возвращать данные. Такое разделение делает код предсказуемым: вызывая метод getUser(), вы уверены, что ничего в системе не изменится.

От CQS к CQRS

CQRS поднимает этот принцип на архитектурный уровень. Если CQS разделяет методы внутри одного объекта, то CQRS разделяет целые модели данных, слои приложения и даже базы данных.

Почему это важно? Потому что требования к чтению и записи в реальных системах настолько различаются, что оптимизировать одну модель для обеих задач становится практически невозможно. Read-операции нуждаются в денормализованных данных для быстрых JOIN-ов, а write-операции требуют нормализации для поддержания консистентности.


            ┌─────────────────────────────────────────────────────────┐
│ Традиционный подход │
├─────────────────────────────────────────────────────────┤
│ Клиент → [Единая модель данных] → База данных │
│ (CRUD операции) │
└─────────────────────────────────────────────────────────┘
          

            ┌─────────────────────────────────────────────────────────┐
│ Подход CQRS │
├─────────────────────────────────────────────────────────┤
│ Клиент → [Command Model] → Write Database │
│ (создание, обновление, удаление) │
│ │
│ Клиент → [Query Model] → Read Database │
│ (чтение, поиск, отчёты) │
└─────────────────────────────────────────────────────────┘
          

Простой пример

Представьте интернет-магазин. В традиционном подходе у вас есть модель Order, которая используется и для создания заказа, и для отображения истории заказов. Звучит логично, но на практике возникают проблемы.

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

Решение с CQRS:

  • Write Model (Command): нормализованные сущности Order, OrderItem, Product со всей бизнес-логикой
  • Read Model (Query): денормализованная проекция OrderView с уже подготовленными данными для отображения

Каждая модель делает свою работу максимально эффективно, без оглядки на требования другой.


2. Как работает CQRS

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

В основе CQRS лежит разделение потоков данных на два независимых пути: путь команд (write path) и путь запросов (read path). Эти пути могут использовать разные технологии, разные базы данных и даже размещаться на разных серверах.

Путь команд (Command Path)

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

Жизненный цикл команды:

  1. Создание команды: клиент формирует объект с данными для действия
  2. Валидация: проверка корректности данных ещё до обработки бизнес-логики
  3. Обработка: выполнение бизнес-логики, проверка инвариантов
  4. Персистентность: сохранение изменений в базу данных
  5. Публикация события: уведомление остальной системы о произошедшем изменении

Важно понимать: команда не возвращает данные. Она либо выполняется успешно, либо выбрасывает исключение. Это следует из принципа CQS и позволяет чётко разделить ответственность.

Пример команды:

ПолеЗначение
ТипCreateOrderCommand
UserId12345
ProductId67890
Quantity2
ShippingAddress«Москва, ул. Примерная, 1»

Путь запросов (Query Path)

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

Жизненный цикл запроса:

  1. Формирование запроса: клиент указывает, какие данные нужны и с какими фильтрами
  2. Обработка: чтение из оптимизированного хранилища (никакой бизнес-логики!)
  3. Возврат данных: передача результата клиенту

Обратите внимание на принципиальное отличие: query-обработчики не содержат бизнес-логики. Они только читают и форматируют данные. Вся логика обработки находится на стороне команд.

Пример запроса:

ПолеЗначение
ТипGetUserOrdersQuery
UserId12345
PageSize10
PageNumber1

Синхронизация моделей

Теперь ключевой вопрос: если у нас две разные модели и, возможно, две разные базы данных — как поддерживать read-модель в актуальном состоянии? Здесь есть два подхода, и выбор между ними определяет характер всей системы.

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

Асинхронная синхронизация (eventual consistency) работает иначе. Write-операция сохраняет данные и публикует событие. Отдельный процесс подхватывает это событие и обновляет read-модель. Write-операции становятся быстрыми, но появляется временное окно, когда данные в read-модели отстают от write-модели — обычно от миллисекунд до нескольких секунд.

ПодходКонсистентностьПроизводительностьСложность
СинхронныйСтрогаяНижеНиже
АсинхронныйEventualВышеВыше

Выбор зависит от бизнес-требований. Для финансовых операций, где критична точность, чаще выбирают синхронный подход. Для социальных сетей и e-commerce, где важна скорость отклика, — асинхронный.

Архитектурная схема CQRS

Собирая всё вместе, получаем следующую архитектуру:


            ┌──────────────┐       ┌──────────────┐
│   Клиент     │       │   Клиент     │
│  (команды)   │       │  (запросы)   │
└──────┬───────┘       └──────┬───────┘
       │                      │
       ▼                      ▼
┌──────────────┐       ┌──────────────┐
│   Command    │       │    Query     │
│   Handler    │       │   Handler    │
└──────┬───────┘       └──────┬───────┘
       │                      │
       ▼                      ▼
┌──────────────┐       ┌──────────────┐
│    Write     │       │     Read     │
│   Database   │──────▶│   Database   │
└──────────────┘       └──────────────┘
        события / проекции
          

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


3. CQRS и Event Sourcing

Когда речь заходит о CQRS, почти всегда упоминается Event Sourcing. Эти паттерны действительно часто используются вместе, но важно понимать: это разные инструменты для разных задач, которые могут применяться независимо друг от друга.

Что такое Event Sourcing

Event Sourcing — это паттерн, который меняет сам принцип хранения данных. Вместо того чтобы сохранять текущее состояние объекта, система сохраняет последовательность событий, которые привели к этому состоянию.

Представьте банковский счёт. Традиционный подход хранит текущий баланс: 50 000 рублей. Event Sourcing хранит историю: открытие счёта, пополнение на 100 000, списание 30 000, ещё одно списание 20 000. Текущий баланс вычисляется путём «проигрывания» всех событий.

Традиционный подход:


            Order {
 id: 123,
 status: "shipped",
 total: 5000,
 items: [...]
}
          

Event Sourcing подход:


            1. OrderCreated { orderId: 123, userId: 456, items: [...] }
2. PaymentReceived { orderId: 123, amount: 5000 }
3. OrderShipped { orderId: 123, trackingNumber: "ABC123" }
          

Зачем это нужно? Event Sourcing даёт полный аудит всех изменений (критично для финансов и регулируемых отраслей), возможность восстановить состояние на любой момент времени и естественный источник событий для интеграции систем.

Почему CQRS и Event Sourcing работают вместе

Синергия этих паттернов неслучайна: Event Sourcing генерирует события как побочный продукт своей работы, а CQRS нуждается в событиях для построения read-моделей. Они дополняют друг друга естественным образом.

ЗадачаCQRSEvent Sourcing
Разделение чтения и записи✅ Основная цель
Аудит и история изменений✅ Основная цель
Построение read-моделей✅ Из событий✅ Генерирует события
Временные запросы (as of date)✅ Восстановление на дату

При совместном использовании Event Store становится единым источником истины (write-модель), а read-модели строятся как проекции из потока событий. Это даёт максимальную гибкость: можно создать сколько угодно read-моделей для разных целей, и все они будут консистентны друг с другом.

Когда использовать их вместе

Комбинация CQRS + Event Sourcing идеальна для определённых сценариев:

Идеальные кейсы:

  • Финансовые системы, где требуется полный аудит каждой операции
  • Системы бронирования, где история изменений критична для разрешения споров
  • Мультитенантные SaaS-платформы, где нужны разные проекции для разных клиентов
  • Системы с жёсткими регуляторными требованиями (GDPR, PCI DSS)

Когда НЕ нужен Event Sourcing:

  • Простые CRUD-приложения без требований к аудиту
  • Прототипы и MVP, где важна скорость выхода на рынок
  • Системы, где данные регулярно удаляются и не требуют истории

Важное замечание

Event Sourcing значительно увеличивает сложность системы: нужно продумать схему событий, версионирование, снапшоты для оптимизации восстановления. Если вам нужен только CQRS для оптимизации производительности, можно обойтись без Event Sourcing, используя синхронную или асинхронную репликацию между write и read моделями.

Не добавляйте Event Sourcing «на всякий случай». Добавляйте, когда бизнес действительно требует полной истории изменений или вам нужны временные запросы.

Иллюстрация

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

CQRS — это не серебряная пуля. Как справедливо отмечает Мартин Фаулер, применение CQRS к неподходящей системе может добавить сложности без существенных выгод. Прежде чем принять решение о внедрении, важно честно ответить на вопрос: решит ли CQRS вашу проблему или создаст новые?

Признаки того, что CQRS вам нужен

1. Диспропорция чтения и записи

Если операций чтения значительно больше, чем записи (10:1 и выше), и требования к ним принципиально различаются, CQRS позволит оптимизировать каждую сторону независимо. Типичная ситуация: чтению нужна низкая задержка и высокая пропускная способность, а записи — сложная валидация, бизнес-правила и транзакционность.

2. Сложные запросы на чтение

Когда для отображения данных требуется собирать информацию из множества источников, делать сложные JOIN'ы и агрегации — это сигнал. Дашборды с метриками из десятка таблиц, аналитические отчёты, поиск с многоуровневой фильтрацией — всё это случаи, где отдельная read-модель даёт кратный прирост производительности.

3. Разные модели для разных потребителей

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

Чек-лист: нужен ли вам CQRS

Честно ответьте на каждый вопрос:

  • [ ] Операций чтения значительно больше, чем записи (10:1 и выше)
  • [ ] Требования к read и write принципиально различаются
  • [ ] Есть проблемы с производительностью запросов
  • [ ] Нужны разные представления одних данных
  • [ ] Система должна масштабироваться независимо по read/write
  • [ ] Бизнес готов на усложнение архитектуры
  • [ ] Команда понимает eventual consistency и готова с ней работать

Если 4+ пунктов — CQRS стоит серьёзно рассмотреть. Если 2-3 — возможно, достаточно более простых оптимизаций: индексы, кэширование, materialized views, денормализация. Начните с простого и усложняйте только при необходимости.

Нужна консультация по выбору архитектуры? Разберём ваш проект и поможем определить, подходит ли CQRS для ваших задач.


На каком этапе ваш проект?

Подключаемся на любой стадии: от идеи до масштабирования. На консультации определим scope и дадим оценку.

Получить оценку

5. Преимущества CQRS

Если вы определили, что CQRS подходит для вашего проекта, полезно понимать полный спектр преимуществ, которые даёт этот паттерн. Некоторые из них очевидны, другие проявляются только со временем.

Производительность и масштабируемость

Главное преимущество CQRS — возможность оптимизировать каждую сторону системы независимо, без компромиссов.

Независимая оптимизация моделей

Read-модель можно оптимизировать исключительно для чтения: использовать денормализацию (избавление от JOIN'ов), предрассчитывать агрегаты, создавать специализированные индексы под конкретные запросы. При этом write-модель остаётся нормализованной, с полной транзакционной целостностью и бизнес-инвариантами — так, как требует надёжная обработка данных.

Независимое масштабирование

В традиционной архитектуре масштабирование базы данных — это всегда компромисс между read и write нагрузками. CQRS позволяет масштабировать их раздельно:

КомпонентСтратегия масштабирования
Read DatabaseМножественные реплики, шардинг по регионам
Write DatabaseМастер с репликацией, вертикальное масштабирование
Query ServiceГоризонтальное масштабирование, кэширование
Command ServiceОчереди команд, rate limiting

Гибкость архитектуры

CQRS открывает возможность использовать разные технологии для разных задач — подход, известный как polyglot persistence.

Разные технологии для разных задач

Write-сторона может использовать PostgreSQL для гарантий ACID и транзакционной целостности. Read-сторона — Elasticsearch для полнотекстового поиска по каталогу, ClickHouse для аналитических дашбордов и Redis для кэширования сессий. Каждая технология решает ту задачу, для которой создана.

Пример polyglot persistence:

МодельТехнологияПричина выбора
Write ModelPostgreSQLACID, транзакции, консистентность
Read Model (каталог)ElasticsearchПолнотекстовый поиск, фасеты
Read Model (аналитика)ClickHouseАгрегации, OLAP-запросы
Read Model (сессии)RedisСкорость, TTL

Упрощение сложных доменов

Разделение на команды и запросы естественно структурирует код и упрощает работу со сложной бизнес-логикой.

Чистая бизнес-логика

Command-обработчики содержат только бизнес-логику: валидацию, проверку прав, применение правил. Query-обработчики содержат только логику представления: форматирование, пагинацию, сортировку. Никаких смешений, никаких компромиссов.

Пример структуры команды:


            CreateOrderCommandHandler:
 1. Проверить, что пользователь существует
 2. Проверить наличие товаров на складе
 3. Рассчитать стоимость с учётом скидок
 4. Создать заказ
 5. Зарезервировать товары
 6. Отправить событие OrderCreated
          

Каждый шаг чётко определён, легко тестируется и модифицируется независимо от того, как данные будут потом отображаться.

Тестируемость

Разделение на команды и запросы упрощает тестирование на нескольких уровнях:

  • Unit-тесты команд проверяют бизнес-логику в изоляции
  • Unit-тесты запросов проверяют правильность выборки данных
  • Интеграционные тесты проверяют синхронизацию между моделями

6. Недостатки и сложности CQRS

CQRS — не бесплатный обед. За преимущества приходится платить увеличением сложности. Важно понимать эту цену до принятия решения о внедрении.

Eventual Consistency

При асинхронной синхронизации возникает задержка между записью и отображением данных. Пользователь создал заказ, нажал «Перейти к заказам» — и не видит свой заказ в списке. Read-модель ещё не обновилась.

Это не баг, это особенность архитектуры. Но её нужно правильно обрабатывать в UI, иначе пользователи будут думать, что система сломана.

Стратегии решения:

СтратегияОписаниеПрименимость
Оптимистичный UIПоказываем ожидаемый результат сразу, до подтвержденияБольшинство случаев
PollingПериодическая проверка обновленияКогда важна точность
WebSocketPush-уведомление о завершении синхронизацииReal-time системы
Redirect after POSTНебольшая задержка перед редиректомВеб-приложения

Операционная сложность

CQRS усложняет не только разработку, но и эксплуатацию системы.

Мониторинг:

  • Нужно отслеживать отставание read-модели (lag) — если оно растёт, что-то не так
  • Мониторинг очередей событий — переполнение означает проблемы
  • Алерты на рассинхронизацию — когда данные расходятся

Debugging:

  • Путь данных неочевиден — нужно понимать, как событие превращается в запись в read-модели
  • Нужны инструменты для просмотра истории событий
  • Correlation ID обязательны для трассировки запросов через всю систему

Дублирование данных

Read-модель содержит денормализованные копии данных. Это занимает дополнительное место и требует ресурсов на синхронизацию. Для больших систем это может быть значительным фактором.

Примерное увеличение объёма хранения:

СценарийУвеличение объёма
Простая денормализация1.5-2x
Множественные проекции3-5x
Event Sourcing5-10x+

Когда сложность не оправдана

Если вы замечаете следующие признаки, возможно, CQRS создаёт больше проблем, чем решает:

  • Команда тратит больше времени на инфраструктуру, чем на бизнес-логику
  • Простые изменения требуют обновления множества компонентов
  • Баги синхронизации становятся основным источником проблем
  • Time-to-market значительно увеличился без пропорционального улучшения качества

В таких случаях стоит пересмотреть решение: возможно, CQRS нужен не везде, а только в самых нагруженных модулях.

Проектируем высоконагруженные системы с нуля и модернизируем существующие. Оставьте заявку для расчёта стоимости вашего проекта.


7. Практическая реализация CQRS

Теория — это хорошо, но как выглядит CQRS в реальном коде? Рассмотрим практические аспекты внедрения на примере типичного e-commerce приложения, где этот паттерн особенно уместен.

Структура проекта

Хорошая организация кода критична для поддерживаемости CQRS-системы. Вот структура, которая зарекомендовала себя в реальных проектах:

Рекомендуемая организация кода:


            src/
├── commands/
│   ├── create-order/
│   │   ├── create-order.command.ts
│   │   ├── create-order.handler.ts
│   │   └── create-order.validator.ts
│   └── ...
├── queries/
│   ├── get-orders/
│   │   ├── get-orders.query.ts
│   │   └── get-orders.handler.ts
│   └── ...
├── domain/
│   ├── order/
│   │   ├── order.aggregate.ts
│   │   └── order.events.ts
│   └── ...
├── read-models/
│   ├── order-view.model.ts
│   └── order-view.projector.ts
└── infrastructure/
    ├── database/
    ├── event-bus/
    └── messaging/
          

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

Пример: Создание заказа

Разберём полный цикл обработки команды создания заказа — от получения запроса до обновления read-модели.

Команда:


            interface CreateOrderCommand {
 userId: string;
 items: Array<{
 productId: string;
 quantity: number;
 }>;
 shippingAddress: Address;
}
          

Команда — это простой объект с данными. Никакой логики, только информация о том, что нужно сделать.

Обработчик команды:


            class CreateOrderHandler {
 async handle(command: CreateOrderCommand): Promise<void> {
 // 1. Загрузить агрегат пользователя
 const user = await this.userRepository.findById(command.userId);
 if (!user) throw new UserNotFoundException();
 
 // 2. Проверить доступность товаров
 const products = await this.inventoryService.checkAvailability(command.items);
 if (!products.allAvailable) throw new InsufficientInventoryException();
 
 // 3. Создать агрегат заказа
 const order = Order.create({
 userId: command.userId,
 items: command.items,
 shippingAddress: command.shippingAddress
 });
 
 // 4. Сохранить
 await this.orderRepository.save(order);
 
 // 5. Опубликовать события
 await this.eventBus.publish(order.getUncommittedEvents();
 }
}
          

Вся бизнес-логика сосредоточена здесь: проверка пользователя, проверка остатков, создание заказа. Обработчик не знает и не заботится о том, как эти данные потом будут отображаться.

Проектор для read-модели:


            class OrderViewProjector {
 @EventHandler(OrderCreatedEvent)
 async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
 const orderView = {
 id: event.orderId,
 userId: event.userId,
 status: 'created',
 items: await this.enrichWithProductDetails(event.items),
 total: this.calculateTotal(event.items),
 createdAt: event.timestamp,
 // Денормализованные данные для быстрого чтения
 customerName: await this.getUserName(event.userId),
 itemCount: event.items.length
 };
 
 await this.readDatabase.upsert('order_views', orderView);
 }
}
          

Проектор слушает события и обновляет read-модель. Обратите внимание на денормализацию: customerName и itemCount хранятся прямо в записи, чтобы не делать JOIN при каждом запросе.

Технологии и инструменты

Выбор технологий зависит от требований проекта. Вот проверенные решения для каждого компонента:

Для событийной шины:

ИнструментПлюсыМинусы
Apache KafkaВысокая пропускная способность, надёжность, replayСложность настройки и операций
RabbitMQПростота, гибкая маршрутизация, широкая поддержкаНиже пропускная способность
Redis StreamsПростота, скорость, низкий порог входаОграничения персистентности
AWS SQS/SNSManaged, масштабируемость, без операционных расходовVendor lock-in

Для read-модели:

ЗадачаТехнология
Общие запросыPostgreSQL с read-репликами
Полнотекстовый поискElasticsearch
АналитикаClickHouse, TimescaleDB
КэшированиеRedis
Графовые запросыNeo4j

Фреймворки и библиотеки

В зависимости от стека разработки есть готовые инструменты, которые упрощают внедрение CQRS:

Java/Kotlin:

  • Axon Framework — полноценный CQRS/ES фреймворк
  • Spring + Kafka — гибкая комбинация для enterprise

Node.js/TypeScript:

  • NestJS CQRS module — встроенная поддержка паттерна
  • EventStoreDB client — для Event Sourcing

.NET:

  • MediatR — легковесная реализация CQRS
  • EventFlow — полный CQRS/ES стек

CQRS в микросервисной архитектуре

В микросервисной архитектуре CQRS проявляет себя особенно эффективно. Каждый сервис может иметь собственную read-модель, оптимизированную под конкретные запросы.

API Gateway и CQRS

API Gateway может маршрутизировать запросы на разные сервисы в зависимости от типа операции:

Тип запросаHTTP методМаршрут
CommandPOST, PUT, DELETE→ Command Service
QueryGET→ Query Service

Это позволяет масштабировать command и query сервисы независимо и применять разные политики (rate limiting, кэширование) к разным типам запросов.

Eventual Consistency между сервисами

В микросервисной архитектуре eventual consistency — это реальность, с которой нужно работать. CQRS не создаёт эту сложность, а делает её явной и управляемой.

Гарантии доставки событий:

УровеньОписаниеПрименение
At-most-onceСобытие может потерятьсяМетрики, логи, некритичные уведомления
At-least-onceСобытие может дублироватьсяБольшинство бизнес-событий
Exactly-onceГарантия единственной доставкиФинансовые операции

Для at-least-once (наиболее распространённый выбор) критически важна идемпотентность обработчиков:


            async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
 // Проверяем, обрабатывали ли уже это событие
 if (await this.isEventProcessed(event.id) {
 return; // Идемпотентность: повторная обработка безопасна
 }
 
 // Обработка события
 await this.createOrderView(event);
 
 // Отмечаем событие как обработанное
 await this.markEventProcessed(event.id);
}
          

Без идемпотентности дублирование событий (неизбежное в распределённых системах) приведёт к дублированию данных в read-модели.

Иллюстрация

Хотите ускорить разработку?

Наша AI-платформа сокращает time-to-market в 2 раза. Расскажем, как это работает на примере вашего проекта.

Узнать подробнее

9. Примеры применения CQRS

Теория и архитектурные схемы — это хорошо, но как CQRS работает в реальных продуктах? Рассмотрим сценарии из разных доменов, где этот паттерн показывает себя особенно эффективно.

E-commerce платформа

Проблема: каталог товаров с миллионами позиций, сложные фильтры и полнотекстовый поиск, при этом — транзакционные операции создания заказов с проверкой остатков.

Попытка решить обе задачи одной моделью приводит к компромиссам: либо поиск медленный (потому что данные нормализованы), либо остатки могут уйти в минус (потому что данные денормализованы и рассинхронизированы).

Решение с CQRS:

МодельХранилищеОптимизация
Write (заказы)PostgreSQLACID, транзакции, FK для целостности
Read (каталог)ElasticsearchПолнотекстовый поиск, фасетные фильтры
Read (история заказов)MongoDBДенормализованные документы для быстрого отображения
Read (аналитика)ClickHouseАгрегации, воронки, когортный анализ

Результат: время поиска снизилось с 500ms до 50ms, нагрузка на основную БД упала на 80%, масштабирование read-части — независимо от write.

Банковское приложение

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

Решение с CQRS + Event Sourcing:

ОперацияРеализация
Перевод средствCommand → Event Store (полный аудит каждой копейки)
Баланс счётаRead Model (предрассчитанный, мгновенный ответ)
История операцийProjection из Event Store
ОтчётностьОтдельная аналитическая БД для регуляторов

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

Результат: полный аудит всех операций, соответствие требованиям ЦБ, мгновенное отображение баланса даже при сотнях транзакций в секунду.

Социальная сеть

Проблема: лента новостей с персонализацией для каждого пользователя, при этом — обработка миллионов постов, реакций и комментариев в реальном времени.

Решение с CQRS:

  • Write Model: создание постов, реакций, комментариев
  • Read Model (лента): pre-computed feed для каждого пользователя
  • Read Model (профиль): агрегированная статистика (количество постов, подписчиков)

Архитектура ленты:


            Пользователь публикует пост
 ↓
 PostCreated Event
 ↓
 Fan-out Service
 ↓
 Обновление лент подписчиков
 (асинхронно, в фоне)
          

Fan-out может занимать секунды для популярных авторов (миллионы подписчиков), но пользователь этого не ждёт — его пост сохранён, а ленты обновятся в фоне.

Логистическая система

Проблема: отслеживание тысяч посылок в реальном времени с множеством интеграций с курьерскими службами. Каждая служба отправляет статусы в своём формате, в своё время.

Решение с CQRS:

КомпонентФункция
Command ServiceПриём и нормализация статусов от курьеров
Read Model (трекинг)Текущий статус для клиента (одна точка правды)
Read Model (аналитика)SLA, среднее время доставки, % проблемных отправлений
Read Model (карта)Геолокация посылок для диспетчеров

Каждая read-модель оптимизирована под свою задачу: трекинг — под быстрый ответ по ID, аналитика — под агрегации за период, карта — под геопространственные запросы.

Разрабатываем e-commerce платформы, банковские приложения и логистические системы. Обсудим ваш проект на бесплатной консультации.


10. Типичные ошибки при внедрении CQRS

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

Ошибка 1: CQRS везде

Проблема: команда принимает решение использовать CQRS и применяет его ко всем модулям системы, включая простые справочники и настройки.

Это приводит к тому, что простая операция «обновить название категории» требует написания Command, CommandHandler, Event, Projector — вместо одного SQL-запроса.

Решение: используйте CQRS только там, где это оправдано диспропорцией нагрузки или сложностью моделей. В одном приложении могут сосуществовать модули с CQRS и без него. Не превращайте архитектурный паттерн в религию.

Ошибка 2: Бизнес-логика в Query handlers

Проблема: разработчики размещают бизнес-правила в обработчиках запросов, нарушая принцип разделения.

Неправильно:


            // Query handler
async getAvailableProducts(): Promise<Product[]> {
 const products = await this.db.query('SELECT * FROM products');
 // Бизнес-логика в query handler — плохо!
 return products.filter(p => p.stock > 0 && p.isActive);
}
          

Правильно:

  • Бизнес-логика — в command handlers и domain
  • Read model уже содержит только доступные товары (фильтрация на этапе проекции)

Query handlers должны быть «тупыми»: принять параметры, выполнить запрос, вернуть данные. Вся умная логика — на стороне команд.

Ошибка 3: Отсутствие идемпотентности

Проблема: обработчики событий не идемпотентны. При повторной доставке события (а это нормальная ситуация в распределённых системах) данные дублируются или повреждаются.

Симптомы:

  • Двойные записи в read model
  • Некорректные агрегаты (счётчики удваиваются)
  • «Потерянные» данные при восстановлении из событий

Решение: каждый обработчик должен корректно обрабатывать повторную доставку события. Проверяйте, обработано ли событие, используйте upsert вместо insert, храните idempotency key.

Ошибка 4: Игнорирование eventual consistency в UI

Проблема: пользователь создаёт сущность, переходит к списку и не видит её — read model ещё не обновилась. Разработчики списывают это на «особенности архитектуры».

Но для пользователя это выглядит как баг: «Я только что создал заказ, а его нет в списке!»

Решения:

СтратегияРеализация
Оптимистичный UIПосле успешной команды добавляем сущность в UI-состояние локально
Redirect with delayПосле команды ждём 100-500ms перед редиректом
Confirmation pageПоказываем страницу подтверждения вместо списка
WebSocket notificationPush-уведомление, когда read model обновилась

Выбор зависит от UX-требований, но игнорировать проблему нельзя.

Ошибка 5: Слишком сложные проекции

Проблема: проектор read-модели разрастается до сотен строк, содержит сложную логику вычислений и становится источником багов.

Признаки:

  • Проектор занимает сотни строк кода
  • При изменении бизнес-логики нужно менять проекцию
  • Частые баги в read model

Решение:

  • Разделите сложную проекцию на несколько простых
  • Вынесите сложные вычисления в scheduled jobs (batch processing)
  • Используйте снимки (snapshots) для сложных агрегаций

Чек-лист: здоровье CQRS-системы

Используйте этот чек-лист для регулярной проверки состояния системы:

  • [ ] Lag read model отслеживается и находится в допустимых пределах
  • [ ] Обработчики событий идемпотентны
  • [ ] Бизнес-логика только в command handlers
  • [ ] UI корректно обрабатывает eventual consistency
  • [ ] Есть мониторинг очередей и dead letter queues
  • [ ] Возможно переиграть проекции с нуля (если Event Sourcing)
  • [ ] Команда понимает архитектуру и может её объяснить

Заключение

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

Ключевые выводы

АспектРекомендация
ПрименимостьИспользуйте при диспропорции read/write 10:1+, сложных запросах, требованиях к масштабированию
СложностьБудьте готовы к увеличению кода в 1.5-2 раза, инфраструктурным расходам
КонсистентностьПримите eventual consistency или используйте синхронную синхронизацию
ПостепенностьНачните с одного bounded context, расширяйте по мере необходимости
МониторингИнвестируйте в observability с первого дня

Альтернативы CQRS

Прежде чем внедрять CQRS, рассмотрите более простые решения:

  1. Read-реплики БД — если нужно только разгрузить основную базу
  2. Кэширование — если проблема в повторяющихся запросах
  3. Materialized Views — если нужны предрассчитанные агрегаты
  4. Денормализация — если проблема в сложных JOIN'ах

CQRS оправдан, когда эти решения недостаточны или невозможны из-за масштаба или сложности системы.

Получите архитектурный аудит вашей системы. Наши эксперты проанализируют текущее решение и предложат оптимизации.

Главный принцип

Архитектура должна соответствовать задаче. Не внедряйте CQRS потому что «так делают в Netflix» — внедряйте потому что это решает конкретную проблему вашего бизнеса, и выгоды перевешивают затраты на усложнение.

Готовы начать разработку?

300+ успешных проектов: от стартапов до enterprise. Прозрачные этапы, фиксированные сроки, гарантия качества.

Обсудить проект

[ обратная связь ]

Расскажите о проекте и мы предложим подходящие решения

напишите нам в Telegram
добавить файл

Отправляя запрос, вы соглашаетесь с политикой конфиденциальности