Gymterview
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-приложений.