CQRS паттерн: что это такое и когда применять в разработке
Полное руководство по паттерну CQRS [2026]
CQRS — это архитектурный паттерн, который решает проблему сложных систем, разделяя модели чтения и записи данных.
Мы в Surf проектируем высоконагруженные системы для крупнейших компаний России и Средней Азии — от банковских приложений до e-commerce платформ с миллионами пользователей. Команда из 250+ специалистов знает: правильный выбор архитектуры на старте экономит месяцы разработки и миллионы рублей в будущем.
В этой статье разберём, что такое CQRS, когда его применять, какие преимущества и недостатки он несёт, и как интегрировать этот паттерн в ваш проект.
Содержание
- Что такое CQRS
- Как работает CQRS
- CQRS и Event Sourcing
- Когда применять CQRS
- Преимущества CQRS
- Недостатки и сложности CQRS
- Практическая реализация CQRS
- CQRS в микросервисной архитектуре
- Примеры применения 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) — это принцип проектирования методов, который помогает сделать код предсказуемым. Идея проста: каждый метод в системе должен выполнять ровно одну из двух задач — либо изменять состояние, либо возвращать данные, но никогда оба действия одновременно.
Ключевое правило: запросы не должны изменять состояние, а команды не должны возвращать данные. Такое разделение делает код предсказуемым: вызывая метод 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)
Команда — это не просто вызов метода, а полноценное описание намерения пользователя изменить систему. Она содержит все данные, необходимые для выполнения действия, и обрабатывается строго определённым образом.
Жизненный цикл команды:
- Создание команды: клиент формирует объект с данными для действия
- Валидация: проверка корректности данных ещё до обработки бизнес-логики
- Обработка: выполнение бизнес-логики, проверка инвариантов
- Персистентность: сохранение изменений в базу данных
- Публикация события: уведомление остальной системы о произошедшем изменении
Важно понимать: команда не возвращает данные. Она либо выполняется успешно, либо выбрасывает исключение. Это следует из принципа CQS и позволяет чётко разделить ответственность.
Пример команды:
Путь запросов (Query Path)
Запрос — это запрос на получение данных без изменения состояния. В отличие от команды, здесь всё просто и линейно: система получает параметры, читает данные из оптимизированного хранилища и возвращает результат.
Жизненный цикл запроса:
- Формирование запроса: клиент указывает, какие данные нужны и с какими фильтрами
- Обработка: чтение из оптимизированного хранилища (никакой бизнес-логики!)
- Возврат данных: передача результата клиенту
Обратите внимание на принципиальное отличие: query-обработчики не содержат бизнес-логики. Они только читают и форматируют данные. Вся логика обработки находится на стороне команд.
Пример запроса:
Синхронизация моделей
Теперь ключевой вопрос: если у нас две разные модели и, возможно, две разные базы данных — как поддерживать read-модель в актуальном состоянии? Здесь есть два подхода, и выбор между ними определяет характер всей системы.
Синхронная синхронизация означает, что при каждой write-операции обновляются обе модели в рамках одной транзакции. Вы получаете гарантию консистентности — данные в read-модели всегда актуальны. Но платите за это увеличением времени отклика: каждая запись становится медленнее.
Асинхронная синхронизация (eventual consistency) работает иначе. Write-операция сохраняет данные и публикует событие. Отдельный процесс подхватывает это событие и обновляет read-модель. Write-операции становятся быстрыми, но появляется временное окно, когда данные в read-модели отстают от write-модели — обычно от миллисекунд до нескольких секунд.
Выбор зависит от бизнес-требований. Для финансовых операций, где критична точность, чаще выбирают синхронный подход. Для социальных сетей и 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-моделей. Они дополняют друг друга естественным образом.
При совместном использовании 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, денормализация. Начните с простого и усложняйте только при необходимости.
На каком этапе ваш проект?
Подключаемся на любой стадии: от идеи до масштабирования. На консультации определим scope и дадим оценку.
5. Преимущества CQRS
Если вы определили, что CQRS подходит для вашего проекта, полезно понимать полный спектр преимуществ, которые даёт этот паттерн. Некоторые из них очевидны, другие проявляются только со временем.
Производительность и масштабируемость
Главное преимущество CQRS — возможность оптимизировать каждую сторону системы независимо, без компромиссов.
Независимая оптимизация моделей
Read-модель можно оптимизировать исключительно для чтения: использовать денормализацию (избавление от JOIN'ов), предрассчитывать агрегаты, создавать специализированные индексы под конкретные запросы. При этом write-модель остаётся нормализованной, с полной транзакционной целостностью и бизнес-инвариантами — так, как требует надёжная обработка данных.
Независимое масштабирование
В традиционной архитектуре масштабирование базы данных — это всегда компромисс между read и write нагрузками. CQRS позволяет масштабировать их раздельно:
Гибкость архитектуры
CQRS открывает возможность использовать разные технологии для разных задач — подход, известный как polyglot persistence.
Разные технологии для разных задач
Write-сторона может использовать PostgreSQL для гарантий ACID и транзакционной целостности. Read-сторона — Elasticsearch для полнотекстового поиска по каталогу, ClickHouse для аналитических дашбордов и Redis для кэширования сессий. Каждая технология решает ту задачу, для которой создана.
Пример polyglot persistence:
Упрощение сложных доменов
Разделение на команды и запросы естественно структурирует код и упрощает работу со сложной бизнес-логикой.
Чистая бизнес-логика
Command-обработчики содержат только бизнес-логику: валидацию, проверку прав, применение правил. Query-обработчики содержат только логику представления: форматирование, пагинацию, сортировку. Никаких смешений, никаких компромиссов.
Пример структуры команды:
CreateOrderCommandHandler:
1. Проверить, что пользователь существует
2. Проверить наличие товаров на складе
3. Рассчитать стоимость с учётом скидок
4. Создать заказ
5. Зарезервировать товары
6. Отправить событие OrderCreated
Каждый шаг чётко определён, легко тестируется и модифицируется независимо от того, как данные будут потом отображаться.
Тестируемость
Разделение на команды и запросы упрощает тестирование на нескольких уровнях:
- Unit-тесты команд проверяют бизнес-логику в изоляции
- Unit-тесты запросов проверяют правильность выборки данных
- Интеграционные тесты проверяют синхронизацию между моделями
6. Недостатки и сложности CQRS
CQRS — не бесплатный обед. За преимущества приходится платить увеличением сложности. Важно понимать эту цену до принятия решения о внедрении.
Eventual Consistency
При асинхронной синхронизации возникает задержка между записью и отображением данных. Пользователь создал заказ, нажал «Перейти к заказам» — и не видит свой заказ в списке. Read-модель ещё не обновилась.
Это не баг, это особенность архитектуры. Но её нужно правильно обрабатывать в UI, иначе пользователи будут думать, что система сломана.
Стратегии решения:
Операционная сложность
CQRS усложняет не только разработку, но и эксплуатацию системы.
Мониторинг:
- Нужно отслеживать отставание read-модели (lag) — если оно растёт, что-то не так
- Мониторинг очередей событий — переполнение означает проблемы
- Алерты на рассинхронизацию — когда данные расходятся
Debugging:
- Путь данных неочевиден — нужно понимать, как событие превращается в запись в read-модели
- Нужны инструменты для просмотра истории событий
- Correlation ID обязательны для трассировки запросов через всю систему
Дублирование данных
Read-модель содержит денормализованные копии данных. Это занимает дополнительное место и требует ресурсов на синхронизацию. Для больших систем это может быть значительным фактором.
Примерное увеличение объёма хранения:
Когда сложность не оправдана
Если вы замечаете следующие признаки, возможно, 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 при каждом запросе.
Технологии и инструменты
Выбор технологий зависит от требований проекта. Вот проверенные решения для каждого компонента:
Для событийной шины:
Для read-модели:
Фреймворки и библиотеки
В зависимости от стека разработки есть готовые инструменты, которые упрощают внедрение 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 может маршрутизировать запросы на разные сервисы в зависимости от типа операции:
Это позволяет масштабировать command и query сервисы независимо и применять разные политики (rate limiting, кэширование) к разным типам запросов.
Eventual Consistency между сервисами
В микросервисной архитектуре eventual consistency — это реальность, с которой нужно работать. CQRS не создаёт эту сложность, а делает её явной и управляемой.
Гарантии доставки событий:
Для 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:
Результат: время поиска снизилось с 500ms до 50ms, нагрузка на основную БД упала на 80%, масштабирование read-части — независимо от write.
Банковское приложение
Проблема: строгие требования к консистентности транзакций (ни рубля не должно потеряться), при этом — высокие требования к скорости отображения баланса (пользователь не хочет ждать).
Решение с CQRS + Event Sourcing:
Event Sourcing здесь не опционален — регуляторные требования обязывают хранить полную историю всех операций. А CQRS позволяет отображать баланс мгновенно, без пересчёта всех транзакций.
Результат: полный аудит всех операций, соответствие требованиям ЦБ, мгновенное отображение баланса даже при сотнях транзакций в секунду.
Социальная сеть
Проблема: лента новостей с персонализацией для каждого пользователя, при этом — обработка миллионов постов, реакций и комментариев в реальном времени.
Решение с CQRS:
- Write Model: создание постов, реакций, комментариев
- Read Model (лента): pre-computed feed для каждого пользователя
- Read Model (профиль): агрегированная статистика (количество постов, подписчиков)
Архитектура ленты:
Пользователь публикует пост
↓
PostCreated Event
↓
Fan-out Service
↓
Обновление лент подписчиков
(асинхронно, в фоне)
Fan-out может занимать секунды для популярных авторов (миллионы подписчиков), но пользователь этого не ждёт — его пост сохранён, а ленты обновятся в фоне.
Логистическая система
Проблема: отслеживание тысяч посылок в реальном времени с множеством интеграций с курьерскими службами. Каждая служба отправляет статусы в своём формате, в своё время.
Решение с CQRS:
Каждая read-модель оптимизирована под свою задачу: трекинг — под быстрый ответ по ID, аналитика — под агрегации за период, карта — под геопространственные запросы.
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 ещё не обновилась. Разработчики списывают это на «особенности архитектуры».
Но для пользователя это выглядит как баг: «Я только что создал заказ, а его нет в списке!»
Решения:
Выбор зависит от 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 — мощный архитектурный паттерн, который решает реальные проблемы масштабирования и производительности. Но, как и любой паттерн, он требует осознанного применения: не ради моды и не потому что «так правильно», а потому что это решает конкретную проблему вашего бизнеса.
Ключевые выводы
Альтернативы CQRS
Прежде чем внедрять CQRS, рассмотрите более простые решения:
- Read-реплики БД — если нужно только разгрузить основную базу
- Кэширование — если проблема в повторяющихся запросах
- Materialized Views — если нужны предрассчитанные агрегаты
- Денормализация — если проблема в сложных JOIN'ах
CQRS оправдан, когда эти решения недостаточны или невозможны из-за масштаба или сложности системы.
Главный принцип
Архитектура должна соответствовать задаче. Не внедряйте CQRS потому что «так делают в Netflix» — внедряйте потому что это решает конкретную проблему вашего бизнеса, и выгоды перевешивают затраты на усложнение.
Готовы начать разработку?
300+ успешных проектов: от стартапов до enterprise. Прозрачные этапы, фиксированные сроки, гарантия качества.