Что такое CQRS (Command Query Responsibility Segregation)
CQRS — паттерн, разделяющий операции чтения (Query) и записи (Command) на отдельные модели. Это похоже на организацию банка: одно окно принимает заявления (command), другое — выдаёт справки (query), и у каждого свой оптимизированный процесс.
В классической архитектуре одна и та же модель используется и для записи, и для чтения. CQRS разделяет их, позволяя оптимизировать каждую сторону независимо.
Схема
Пример
┌──────────────────────────────────┐
│ API Gateway │
└────────┬────────────┬────────────┘
│ │
Command│ │Query
▼ ▼
┌──────────┐ ┌──────────────┐
│ Command │ │ Query │
│ Model │ │ Model │
│ (Write) │ │ (Read) │
└────┬─────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Write │ │ Read DB │
│ DB │──▶ (денормал.) │
│(PostgreSQL)│ │ (Redis / │
└──────────┘ │ Elasticsearch)│
синхронизация └────────────┘
через события
Пример кода
Пример кода
// Command — изменяет состояние, ничего не возвращает
public interface CreatePaymentCommandHandler {
void handle(CreatePaymentCommand command);
}
@Service
public class CreatePaymentCommandHandlerImpl implements CreatePaymentCommandHandler {
private final PaymentWriteRepository repository;
private final EventPublisher eventPublisher;
@Override
@Transactional
public void handle(CreatePaymentCommand command) {
Payment payment = Payment.create(command);
repository.save(payment);
eventPublisher.publish(new PaymentCreatedEvent(payment));
}
}
// Query — только читает, не изменяет состояние
public interface PaymentQueryService {
PaymentView findById(Long id);
List<PaymentView> findByClientId(Long clientId);
}
@Service
public class PaymentQueryServiceImpl implements PaymentQueryService {
private final PaymentReadRepository readRepository; // оптимизирован для чтения
@Override
public PaymentView findById(Long id) {
return readRepository.findById(id);
}
}
Когда применять CQRS
- Нагрузка на чтение значительно превышает нагрузку на запись (типично для банковских систем: просмотр выписок vs. совершение платежей).
- Модели для чтения и записи сильно отличаются.
- Нужна независимая масштабируемость чтения и записи.
- Система использует Event Sourcing.
Когда не стоит применять
- Простые CRUD-приложения.
- Когда eventual consistency неприемлема.
- Маленькие проекты с одной командой.
Связь с Event Sourcing
CQRS часто комбинируют с Event Sourcing — вместо хранения текущего состояния хранится полная последовательность событий, из которых состояние можно восстановить. Это даёт полный аудит изменений и возможность «перемотки» состояния на любой момент времени, но значительно усложняет реализацию.
На собеседовании: Интервьюер хочет услышать, когда CQRS оправдан, а когда это overengineering. Частая ошибка — считать, что CQRS всегда требует двух баз данных; на деле можно начать с разделения моделей в одной БД.