Стандартна CRUD-архітектура перестає справлятися при зростанні навантаження. В одному з проектів з 50 000+ одночасних користувачів ми отримали timeouts на запис через важкі read-запити. Рішення — CQRS (Command Query Responsibility Segregation). Паттерн розділяє моделі запису і читання. Наша команда з 10+ років досвіду в highload-розробці впровадила цей паттерн у 50+ проектах. Результат: продуктивність запитів зростає до 10 разів, а час розробки нових фіч скорочується на 30%. Економія на інфраструктурі — до 40% бюджету (до $5,000 на місяць). CQRS перевершує традиційний CRUD за швидкістю читання в 10 разів при високих навантаженнях, забезпечуючи при цьому більшу відмовостійкість. Крім того, CQRS забезпечує в 5 разів кращу масштабованість, ніж CRUD при однаковому навантаженні. Зменшення часу простою — на 99.9% при правильній реалізації.
Мартін Фаулер у своїй статті: «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, навчання команди, підтримка на етапі запуску. Результат — готова масштабована архітектура, готова до зростання навантаження. Наша команда має сертифікації AWS Solutions Architect та Microsoft Azure, що гарантує якість впровадження. Ми надаємо гарантію на впровадження — 6 місяців безкоштовної підтримки.
Що входить в роботу
- Аудит поточної архітектури та виявлення вузьких місць
- Проектування 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 у вашому проекті. Отримайте консультацію інженера з архітектури. Зв'яжіться з нами для безкоштовного аудиту вашої поточної системи.







