senior
Что такое CQRS?
CQRS (Command Query Responsibility Segregation) — это паттерн, разделяющий модели чтения и записи данных. Команды (Commands) изменяют состояние, запросы (Queries) читают данные. Для каждой задачи используется своя оптимизированная модель.
Пример
┌─────────────────┐
│ API Gateway │
└───────┬─────────┘
┌──────┴──────┐
│ │
┌──────▼──┐ ┌─────▼─────┐
│ Command │ │ Query │
│ Model │ │ Model │
└──────┬───┘ └─────▲─────┘
│ │
┌──────▼───┐ ┌────┴──────┐
│ Write DB │──►│ Read DB │
│(PostgreSQL)│ │(Elasticsearch,│
└───────────┘ │ Redis, etc.) │
└──────────────┘
Зачем нужен CQRS
- Разная оптимизация — модель записи нормализована (3NF), модель чтения денормализована для быстрых запросов.
- Масштабирование — чтений обычно гораздо больше, чем записей (соотношение 100:1). Можно масштабировать read-модель отдельно.
- Разные хранилища — запись в PostgreSQL, чтение из Elasticsearch или Redis.
- Естественно сочетается с Event Sourcing — события из write-модели проецируются в read-модель.
Пример реализации CQRS
// Command — изменение данных
public record TransferMoneyCommand(
UUID fromAccountId,
UUID toAccountId,
BigDecimal amount
) {}
@Service
@RequiredArgsConstructor
public class AccountCommandService {
private final AccountRepository accountRepository;
private final EventPublisher eventPublisher;
@Transactional
public void handle(TransferMoneyCommand cmd) {
Account from = accountRepository.findById(cmd.fromAccountId());
Account to = accountRepository.findById(cmd.toAccountId());
from.withdraw(cmd.amount());
to.deposit(cmd.amount());
accountRepository.save(from);
accountRepository.save(to);
eventPublisher.publish(new MoneyTransferredEvent(
cmd.fromAccountId(), cmd.toAccountId(), cmd.amount()));
}
}
// Query — чтение данных из оптимизированной read-модели
public record AccountBalanceQuery(UUID accountId) {}
@Service
@RequiredArgsConstructor
public class AccountQueryService {
private final AccountReadRepository readRepository; // Может быть Redis, ES
public AccountBalanceView handle(AccountBalanceQuery query) {
return readRepository.findById(query.accountId())
.orElseThrow(() -> new AccountNotFoundException(query.accountId()));
}
}
// Read-модель (денормализованный view)
@Document(indexName = "account-balances") // Elasticsearch
public class AccountBalanceView {
private UUID accountId;
private String customerName;
private BigDecimal balance;
private String currency;
private LocalDateTime lastTransactionDate;
private int totalTransactions;
}
// Проекция — обновление read-модели при получении событий
@Service
public class AccountBalanceProjection {
@KafkaListener(topics = "account-events")
public void project(AccountEvent event) {
switch (event) {
case MoneyDeposited e -> readRepository.updateBalance(
e.accountId(), e.amount());
case MoneyWithdrawn e -> readRepository.updateBalance(
e.accountId(), e.amount().negate());
}
}
}
CQRS добавляет сложность. Используйте его только когда есть реальная необходимость:
- Существенная разница в нагрузке чтение/запись
- Разные требования к моделям чтения и записи
- Необходимость поддержки сложных запросов (полнотекстовый поиск, агрегации)
На собеседовании: объясните связку CQRS + Event Sourcing: события из write-модели проецируются в read-модель. Обязательно упомяните eventual consistency как следствие разделения моделей. Частая ошибка — предлагать CQRS для простых CRUD-приложений.