Стандартная CRUD-архитектура перестаёт справляться при росте нагрузки. В одном из проектов с 50 000+ одновременных пользователей мы получили timeouts на запись из-за тяжёлых read-запросов. Решение — CQRS (Command Query Responsibility Segregation). Паттерн разделяет модели записи и чтения. Наша команда с 10+ лет опыта в highload-разработке внедрила этот паттерн в 50+ проектах. Результат: производительность запросов вырастает до 10 раз, а время разработки новых фич сокращается на 30%. Экономия на инфраструктуре — до 40% бюджета. CQRS превосходит традиционный CRUD по скорости чтения в 10 раз при высоких нагрузках, обеспечивая при этом большую отказоустойчивость.
Мартин Фаулер в своей статье: «CQRS — это не серебряная пуля, но в правильных руках он творит чудеса.»
Почему CRUD ломается под нагрузкой?
Типичные проблемы: N+1 запросы, блокировки при записи, неоптимальные индексы. Когда читают в 10 раз чаще, чем пишут, одна модель данных не может удовлетворить оба сценария. CQRS решает это разделением, но цена — сложность. Для простого CRUD он избыточен. Мы рекомендуем начинать с логического разделения и переходить к разным БД только при доказанной необходимости.
Разделение команд и запросов на примере интернет-магазина
В проекте с 100 000 товаров и 10 000 заказов в час мы внедрили CQRS. Write model на PostgreSQL, read model на Redis. Время ответа каталога упало с 2 секунд до 200 мс — в 10 раз. Заказчик отметил, что затраты на разработку окупились за 3 месяца за счёт снижения нагрузки. Команды обрабатываются через CommandBus, запросы — через QueryBus. Синхронизация — через доменные события. Eventual consistency допустима для большинства UI.
Структура Command и Query сторон
Command Side — намерение изменить состояние. Команды — неизменяемые объекты. Обработчик загружает агрегат, проверяет бизнес-правила и сохраняет.
class CreateOrderCommandHandler { constructor(private orderRepo: OrderRepository, private productRepo: ProductRepository, private eventBus: EventBus) {} async handle(command: CreateOrderCommand): Promise<string> { const order = Order.create(command.customerId); for (const item of command.items) { const product = await this.productRepo.findById(item.productId); if (!product.isAvailable(item.quantity)) throw new InsufficientStockError(item.productId); order.addItem(item); } order.setShippingAddress(command.shippingAddress); order.submit(); await this.orderRepo.save(order); await this.eventBus.publishAll(order.pullDomainEvents()); return order.id; } } Query Side — запрос данных без побочных эффектов. Read Model — денормализованное представление, оптимизированное под конкретный UI. Хендлер запроса читает напрямую из Read Model.
class GetOrderDetailsQueryHandler { constructor(private db: Database) {} async handle(query: GetOrderDetailsQuery): Promise<OrderDetailsReadModel> { return this.db.queryOne(` SELECT o.id, o.status, o.created_at, o.updated_at, c.id as customer_id, c.name as customer_name, c.email, json_agg(json_build_object( 'productId', oi.product_id, … )) as items, o.shipping_address, o.total_amount FROM orders_view o JOIN customers c ON c.id = o.customer_id JOIN order_items_view oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id WHERE o.id = $1 GROUP BY o.id, c.id `, [query.orderId]); } } Синхронизация Read Model с Write Model через доменные события
Read Model обновляется асинхронно через доменные события. Это даёт eventual consistency — возможна кратковременная задержка (обычно до 1 секунды), но система остаётся отзывчивой.
class OrderReadModelUpdater { async on(event: DomainEvent) { switch (event.eventType) { case 'OrderCreated': await this.db.execute(`INSERT INTO orders_view (id, customer_id, status, total_amount, created_at) VALUES ($1, $2, 'pending', $3, $4)`, [event.aggregateId, event.payload.customerId, event.payload.total, event.occurredAt]); break; case 'OrderStatusChanged': await this.db.execute(`UPDATE orders_view SET status = $2, updated_at = $3 WHERE id = $1`, [event.aggregateId, event.payload.newStatus, event.occurredAt]); break; } } } Какие риски и сложности внедрения CQRS?
Основные риски — увеличение сложности системы, eventual consistency (read model может отставать), необходимость синхронизации данных. Также требуется опытная команда, знающая DDD и Event Sourcing. Начинать стоит с логического разделения, а уже затем переходить к разным БД. В типичных проектах мы используем TypeScript и Nest.js, а для событий — Kafka. Для read model часто выбираем Redis или ElasticSearch в зависимости от нагрузок.
Пошаговая реализация CQRS: чек-лист
- Выделите Command и Query в отдельные интерфейсы.
- Реализуйте
CommandBusиQueryBusс middleware. - Разделите модели данных: нормализованная для записи, денормализованные view для чтения.
- Настройте асинхронную синхронизацию через Event Bus.
- Протестируйте eventual consistency и производительность.
Масштабирование чтения и записи: от одной БД до микросервисов
Write Side масштабируется вертикально или шардированием по aggregate_id. Read Side — горизонтально: read replicas PostgreSQL, Redis для горячих данных, Elasticsearch для полнотекстового поиска. Каждый Read Model может иметь отдельную таблицу или схему. Переход от логического разделения к разным сервисам увеличивает сложность, но даёт максимальную гибкость.
| Уровень | Описание | Сложность |
|---|---|---|
| Логическое разделение | Отдельные методы/классы для команд и запросов | Низкая |
| Разные модели данных | Команды → нормализованная БД, запросы → денормализованные view | Средняя |
| Разные базы данных | Write DB (PostgreSQL), Read DB (Redis/Elastic) | Высокая |
| Разные сервисы | Write и Read — отдельные микросервисы с независимым деплоем | Очень высокая |
Сравнение производительности до и после CQRS
| Метрика | До CQRS | После CQRS |
|---|---|---|
| Время ответа каталога | 2 с | 200 мс |
| Пропускная способность запросов | 500 req/s | 5000 req/s |
| Загрузка CPU на запись | 80% | 30% |
Процесс внедрения CQRS в проект
Мы предлагаем внедрение CQRS под ключ: аудит текущей архитектуры, проектирование Command и Query моделей, реализация с примером кода на TypeScript (Nest.js / Express), настройка синхронизации через Event Bus, документирование API, обучение команды, поддержка на этапе запуска. Результат — готовая масштабируемая архитектура, готовая к росту нагрузки.
Что входит в работу
- Аудит текущей архитектуры и выявление узких мест
- Проектирование Command и Query моделей с учётом доменной логики
- Реализация на TypeScript с использованием Nest.js или Express
- Настройка асинхронной синхронизации через Event Bus (Kafka/RabbitMQ)
- Документирование API и read-моделей
- Обучение команды работе с CQRS и eventual consistency
- Поддержка на этапе запуска и первого релиза
Сроки реализации
- Рефакторинг существующего приложения к Command/Query разделению — 1–2 недели.
- Новое приложение с CQRS от старта — 2–3 недели.
- Полный CQRS + Event Sourcing + async Read Models — 4–8 недель в зависимости от сложности домена.
Закажите внедрение CQRS в вашем проекте. Получите консультацию инженера по архитектуре. Свяжитесь с нами для бесплатного аудита вашей текущей системы.







