Gymterview
senior

Что такое 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 всегда требует двух баз данных; на деле можно начать с разделения моделей в одной БД.