Коротко о самом принципе. CQS (Command–Query Separation) говорит: метод либо команда — делает побочный эффект и не возвращает (или не обещает) значимое вычисляемое значение, либо запрос — возвращает данные и не имеет побочных эффектов. На практике это правило помогает разделять ответственность: кто меняет состояние, а кто читает его.
Что относится к «commands» (командной стороне)
- Операции, которые изменяют состояние системы: создание, удаление, любые изменения сущностей и агрегатов, транзакции, переключение состояний (approve, reject, publish, cancel и т. п.).
- Оркестрация сценариев применения бизнес‑логики: загрузка агрегата, вызов доменных методов на агрегате, проверка invariants, сохранение через репозиторий.
- Кросс‑срезовые обязанности, связанные с изменением состояния: валидация (application/command validation), авторизация действий, открытие/закрытие транзакций, публикация доменных/интеграционных событий, логирование, реализация идемпотентности, retry, компенсации и т. п.
- Вызовы внешних сервисов, которые являются частью выполнения команды (например уведомление после успешной модификации).
Почему «update» в командном сервисе кажется лишним
- Часто действительно лучше избегать обобщённого метода update(entityDto). Вместо этого предпочтительнее явно моделировать намерение: ChangeCustomerAddress, ApproveOrder, AddItemToCart. Так легче инкапсулировать бизнес‑правила и понять ответственность.
- Само изменение состояния должно происходить в доменной сущности/агрегате (неанемичная модель): command handler/сервис загружает агрегат, вызывает агрегатный метод (например order.addItem(...)), затем сохраняет агрегат через репозиторий.
Что размещать в командном сервисе помимо save
- Конкретные команды/handlers с описанными выше обязанностями (валидировать команду, проверять авторизацию, загружать агрегат, вызывать доменные методы, сохранять, публиковать события).
- Утилиты, связанные с выполнением команд (идемпотентность, трекинг, транзакции).
- Репозитории для загрузки/сохранения агрегатов (или вызовы на них). Но не презентационные запросы/проекции.
SELECT‑запросы: когда они относятся к commands, а когда к queries
- SELECT, который делается только внутри команды для того, чтобы загрузить агрегат/проверить условие перед изменением — это часть реализации команды. Он не является "запросом" в смысле CQS, потому что используется для подготовки побочного эффекта. Такие загрузки должны идти через репозиторий/агрегат‑фабрику.
- SELECT, который возвращает данные для отображения/отчёта/интерфейса — это query и принадлежит read‑сервису/проекциям/DAO для чтения.
- Внутри команды допустимы селекты для валидаций (например «есть ли у пользователя доступ», «существует ли товар»). Но это не превращает команду в query.
Если один и тот же запрос/SQL используется и для изменения, и для отображения
- Логика разделения по назначению, а не по SQL: для модификации используйте репозиторий/агрегат (rich domain object) — вы загружаете состояние в доменную модель, применяете поведение и сохраняете; для отображения — используйте отдельные read‑проекции/DTO, оптимизированные под чтение.
- Если технически вы дублируете SQL/проекцию, можно вынести низкоуровневый доступ (например DAO или общие SQL‑фрагменты), но сохранить семантическое разделение: вызовы для чтения — в query‑слое, для загрузки агрегата — в repository command‑слое.
- Не объединяйте публичные API: не возвращайте из команды UI DTO; команды возвращают только статус/идемпотентный результат/идентификатор, а запросы — данные для отображения.
Практические рекомендации
- Модель команд как «интент»: именуйте методы конкретно (ApproveInvoice, ChangePassword), а не generic update.
- Держите domain logic внутри сущностей/агрегатов; command handlers — оркестраторы.
- Репозитории отвечают за загрузку/сохранение агрегатов; query‑репозитории/проекции — за быстрые SELECT для UI.
- SELECTы, используемые только для проверки/загрузки в рамках изменения, являются частью командной реализации и не должны становиться общедоступными query‑методами.
- Если экономия кода подталкивает к совместному использованию SQL — делайте это через общий низкоуровневый слой, но держите семантическое разделение слоёв.
Итог
Commands — это всё, что меняет состояние + связанные оркестрационные обязанности (транзакция, валидация, авторизация, публикация событий). SELECT‑запросы, служащие только для загрузки агрегата перед изменением, — часть реализации команды (через репозиторий). SELECT для отображения — в query/ReadModel. Если один и тот же SQL нужен и там, и там — можно вынести общий DAO, но не смешивайте слои на уровне API и семантики.