Python CQRS Application Layer Writing
Общие правила оформления Python-кода брать из $python-code-style-writing и не
дублировать здесь; этот скил определяет только контракты application-слоя.
Quick Start
- Извлеки из задачи точные команды, запросы, входы, результаты, порты, порядок шагов, авторизацию, границы согласованности и публичные исходы. Не дополняй контракт типовыми элементами.
- Размести операцию по бизнес-возможности;
user/system используй только при заданном различии.
- Объяви Command, Query и структурные публичные DTO как глубоко неизменяемые
@dataclass(slots=True, frozen=True): коллекции — tuple, вложения — другие
неизменяемые DTO. Заданное DTO-перечисление ожидаемых результатов реализуй
через стандартный Enum, без Pydantic и transport-значений.
- Для partial update различай
UNSET, None и переданное значение. Не используй truthiness вместо проверки контракта.
- Валидируй структурные ограничения входа в use case до I/O. Повторяемую
проверку общего input DTO реализуй один раз в helper базового use case, но
вызывай явно в каждой операции, которой она нужна. DTO оставляй без
поведения и валидации.
- Преобразуй вход в domain VO до транзакции, если преобразованию не нужен I/O.
- Инжектируй только требуемые порты. Для заданной атомарной группы используй обязательную фабрику нового UoW; независимый порт передавай напрямую.
- Для нового агрегата получай техническое значение ID через типизированный
исходящий порт, оборачивай его в доменный VO и передавай фабрике. Не требуй
новый ID во входной команде без заданной внешней семантики.
- Реализуй только заданную авторизацию и порядок доменных вызовов. Не добавляй инициатора, роли и policy по шаблону.
- Оборачивай
execute явным application-декоратором преобразования DomainError: стабильные типы сопоставляй общей картой, operation-specific типы — override-картой use case, неизвестный отказ — в AppInternalError.
- Техническую ошибку адаптер преобразует в
AppPortError, а use case — в публичный исход своей операции.
- Формируй самодостаточные неизменяемые version/outbox DTO в заданных точках
сохранения. Для закрытого объединения конкретных DTO обязательно объявляй
явный
TypeAlias и используй его в outbox/publisher-портах; не реализуй event
sourcing и не вводи enum ресурсов только для dispatch.
- Храни в use case только зависимости, а данные вызова — в локальных переменных. Не кешируй результаты в экземпляре.
- Оставляй в
execute явную оркестрацию преобразования, загрузки,
авторизации, доменной операции и сохранения. Каждый helper выполняет только
один шаг и не возвращает кортеж разнородных загруженных объектов.
- Не создавай application-сценарии и не вызывай один use case из другого. Реализуй однозначно заданные части без придумывания семантики.
- Не логируй в application-слое, включая use case, UoW и реализации портов,
расположенные в этом слое. Сохраняй безопасный контекст в типизированных
результатах и ошибках.
When to Apply
Триггеры активации
По расположению файлов — любая правка/создание в application/:
application/command/<business_capability>/{user,system}/<action>.py
application/query/<business_capability>/{user,system}/<action>.py
application/command/base.py, application/query/base.py
application/dto/<aggregate>.py, application/dto/paginator.py
application/port/unit_of_work.py, application/port/event_publisher.py
application/port/repository/__init__.py, application/port/repository/<aggregate>.py
application/error.py
По концепциям, упомянутым пользователем:
- CQRS-термины: «use case», «сценарий», «command», «команда» в смысле прикладного действия, «query», «запрос».
- DTO, маппинг доменного объекта в DTO.
- Unit of Work, UoW, граница транзакции на уровне сценария.
- Порт, port, адаптер; EventPublisher; outbox-публикация.
- Иерархия прикладных ошибок (
AppError, AppNotFoundError, AppInvalidDataError, AppInternalError).
- Авторизация инициатора, role check.
- Прикладной слой, application layer.
По типам задач:
- Добавление нового use case (команды или запроса).
- Добавление нового DTO.
- Расширение
UnitOfWork под новый агрегат.
- Добавление нового интерфейса репозитория или новой группы (
outbox, subscription).
- Добавление события в outbox + соответствующий publisher use case.
- Расширение иерархии прикладных ошибок.
- Правка списочных запросов (filter helper, paginator).
- Добавление источника идентификаторов, часов, кеша или другого исходящего порта.
Анти-триггеры
Скил не активируется, когда правка относится к слоям domain/, infrastructure/ или presentation/ — для редактирования каждого из этих слоёв используется свой скил:
domain/ — доменная модель: агрегаты, value objects, фабрики, доменные сервисы, абстрактные domain-интерфейсы.
infrastructure/ — реализации портов: репозитории к БД, адаптеры к брокеру сообщений, миграции схемы БД.
presentation/ — точки входа в систему: HTTP-эндпоинты, фоновые worker-ы, схемы валидации входящих данных.
- Составные сценарии и pipeline-описания: отдельные классы сценариев в
application/ не создаются, один use case из другого не вызывается.
Также не активируется на косметические правки внутри application/ (только docstrings, переименование без изменения контракта, импорт-рефакторинг).
Именование структуры
- Предпочитай единственное число в каталогах и файлах:
command, query, port, repository, error.py.
- Сохраняй уже принятую единообразную конвенцию проекта, если пользователь не согласовал переименование.
- Не включай массовое переименование в функциональную задачу без явного согласия.
- Все пути далее — иллюстрации; подставляй согласованную форму единственного или множественного числа последовательно.
Предусловия
- Гексагональная архитектура с выделенным
application/-слоем.
- CQRS: команды и запросы разнесены по разным деревьям.
application/ импортирует только из domain/ и стандартной библиотеки. Запрещены импорты из infrastructure/, presentation/. Внешние библиотеки (web-фреймворки, драйверы БД, клиенты брокеров и т.п.) не используются.
- Состав и семантика application-операций заданы входными требованиями. Скил
выбирает Python-реализацию, но не проектирует новые поля, исходы и шаги.
- Прикладная авторизация использует только заданные сведения и domain-контракты.
Аутентификация и transport context остаются во входящем адаптере.
Package Structure
application/
├── __init__.py ← пустой
├── error.py ← иерархия AppError
│
├── dto/
│ ├── __init__.py ← пустой
│ ├── unset.py ← UNSET для partial update
│ ├── paginator.py ← LimitOffsetPaginator и общие пагинаторы
│ └── <meaning>.py ← публичные application DTO
├── error_mapping/
│ └── domain.py ← единая граница domain-ошибок
│
├── port/
│ ├── __init__.py ← пустой
│ ├── unit_of_work.py ← UnitOfWork + обязательная UnitOfWorkFactory
│ ├── identifier.py ← только требуемые типизированные источники ID
│ ├── event_publisher.py ← EventPublisher ABC + TypeAlias событий
│ ├── dto/ ← самодостаточные port DTO
│ │ └── <meaning>.py
│ └── repository/
│ ├── __init__.py ← только re-export + __all__
│ └── <aggregate>.py ← интерфейсы + <Aggregate>Repositories
│
├── command/
│ ├── __init__.py ← пустой
│ ├── base.py ← только реально общие helpers
│ └── <business_capability>/
│ ├── __init__.py ← публичная витрина возможности
│ ├── user/ ← только при заданном разделении
│ │ └── <action>.py
│ └── system/ ← только при заданном разделении
│ └── <action>.py
│
└── query/
├── __init__.py ← пустой
├── base.py ← только реально общие helpers
└── <business_capability>/
├── __init__.py ← публичная витрина возможности
├── user/
│ └── <action>.py
└── system/
└── <action>.py
Для малого набора операций сохраняй принятую плоскую структуру. Группируй по
бизнес-возможности при росте: более 12 файлов на уровне, минимум две устойчивые
группы с двумя операциями, повторяющиеся префиксы, необходимость индекса или
очевидное скорое достижение этих условий. Не перемещай существующие файлы
автоматически. Уровень bounded context добавляй только при явно выделенных
нескольких контекстах и устойчивой группировке возможностей.
Назначение узлов
Ветки user и system одинаково применяются к командам и запросам:
user и system отражают заданный источник операции, но сами по себе не
добавляют initiator_id, роль или policy. Включай только сведения и проверки
конкретного контракта.
Эта классификация не определяет доступность операции через HTTP или брокер и не
является заменой модификаторам видимости.
| Узел |
Что внутри |
Канон |
error.py |
AppError + AppNotFoundError, AppInvalidDataError, AppInternalError |
один файл, не дробится |
dto/<meaning>.py |
Публичные application DTO, названные по смыслу данных |
по связному представлению |
dto/unset.py |
Общий UNSET для partial update |
один файл |
dto/paginator.py |
общие пагинаторы (LimitOffsetPaginator) |
один на проект |
port/unit_of_work.py |
UnitOfWork ABC + обязательная фабрика нового экземпляра |
один файл |
port/identifier.py |
типизированные источники новых ID |
только при потребности |
port/event_publisher.py |
EventPublisher ABC + TypeAlias допустимых event DTO |
один файл |
port/dto/<meaning>.py |
Самодостаточные внутренние DTO портов |
создаётся по необходимости |
port/repository/__init__.py |
Только re-export и алфавитный __all__ |
публичная витрина |
port/repository/<aggregate>.py |
Repository-интерфейсы и <Aggregate>Repositories |
по одному файлу на агрегат |
command/base.py / query/base.py |
базовые классы use case-ов (если приняты в проекте) |
пример конвенции, не предписание |
command/<business_capability>/.../<action>.py |
один use case = один файл |
группировка по бизнес-возможности |
query/<business_capability>/.../<action>.py |
dataclass-запрос + use case-класс |
то же |
error_mapping/domain.py |
Декоратор границы execute, стабильная карта и operation-specific overrides |
один файл |
Конвенции по __init__.py
Не создавай пустые leaf-пакеты возможностей или источников операций и не
оставляй __all__ = [] как заглушку. При удалении или переносе последней
операции удали ставшие пустыми __init__.py и каталог. Ветки user и system
существуют только пока содержат хотя бы одну актуальную операцию.
| Уровень |
Состояние |
Содержимое |
application/__init__.py |
пустой |
— |
application/{command,query,dto,port}/__init__.py |
пустой |
— |
application/{command,query}/<business_capability>/__init__.py |
публичная витрина |
операции возможности |
application/command/<business_capability>/__init__.py |
не пустой |
re-export публичных use case и Command возможности |
application/query/<business_capability>/__init__.py |
не пустой |
re-export публичных use case и Query возможности |
application/dto/__init__.py |
пустой |
DTO импортируются напрямую из application.dto.<aggregate> |
application/port/__init__.py |
пустой |
— |
application/port/repository/__init__.py |
не пустой |
только re-export контрактов и алфавитный __all__ |
Входящие адаптеры импортируют операции через витрину бизнес-возможности.
Изнутри application используй прямые пути.
Naming convention для use case-ов (CQRS verb-first)
Команды — императивный verb + аггрегат:
| Файл |
Command |
UseCase |
create.py |
Create<Aggregate>Command |
Create<Aggregate>UseCase |
update.py |
Update<Aggregate>Command |
Update<Aggregate>UseCase |
delete.py |
Delete<Aggregate>Command |
Delete<Aggregate>UseCase |
restore.py |
Restore<Aggregate>Command |
Restore<Aggregate>UseCase |
publish.py |
— (нет входа) |
Publish<Aggregate>VersionsUseCase |
<verb>_<object>.py (доменный) |
<Verb><Aggregate><Object>Command |
<Verb><Aggregate><Object>UseCase |
Запросы — Get / List + описание возвращаемого результата:
| Файл |
Query |
UseCase |
retrieve_last_version.py |
Get<Aggregate>LastVersionQuery |
Get<Aggregate>LastVersionUseCase |
retrieve_version.py |
Get<Aggregate>VersionQuery |
Get<Aggregate>VersionUseCase |
list_last_versions.py |
List<Aggregate>LastVersionsQuery |
List<Aggregate>LastVersionsUseCase |
list_versions.py |
List<Aggregate>VersionsQuery |
List<Aggregate>VersionsUseCase |
Имена файлов следуют столбцу «Файл» — это часть структуры слоя, не имя класса.
Тип сохранённого изменения называй по смыслу (ProjectChange, TenantChange) и
размещай рядом с version DTO. Это application-метаданные снимка, не доменное
событие и не transport message.
Чего в layout-е нет и почему
system и user обозначают источник сценария, а не публичность Python API или транспортную доступность.
- Ветки
command/{user,system}/ и query/{user,system}/ создавай только при
наличии реальной операции; пустые ветки заранее не добавляй и после удаления
последней операции не сохраняй.
- Не создавай event-sourcing API,
events.py и коллекцию событий агрегата.
Errors Hierarchy
Полная иерархия
AppError (msg, action, data)
├── AppNotFoundError — основной ресурс не найден
├── AppInvalidDataError — зависимость/контекст невалиден или отсутствует
├── AppForbiddenError — операция запрещена прикладной политикой
├── <PublicOutcomeError> — различимый публичный исход операции
├── AppInternalError — непредусмотренный внутренний исход (+ wrap_error)
└── AppPortError — внутренняя ошибка исходящего порта (+ wrap_error)
Иерархия живёт в application/error.py. Создавай отдельный публичный тип для
каждого ошибочного исхода, который вызывающая сторона должна различать. Ожидаемые
управляющие результаты возвращай заданным DTO и не представляй исключениями. Не
создавай разные типы для одинаковой публичной семантики. AppPortError не
пересекает входящую границу: use case всегда преобразует его в публичный
ошибочный исход или AppInternalError.
Базовый класс
from typing import Any
class AppError(Exception):
def __init__(
self,
msg: str,
action: str,
data: dict[str, Any] | None = None,
*args: object,
) -> None:
super().__init__(msg, *args)
self.msg = msg
self.action = action
self.data = data or {}
class AppNotFoundError(AppError):
pass
class AppInvalidDataError(AppError):
pass
class AppInternalError(AppError):
def __init__(
self,
msg: str,
action: str,
data: dict[str, Any] | None = None,
wrap_error: BaseException | None = None,
*args: object,
) -> None:
super().__init__(msg, action, data, *args)
self.wrap_error = wrap_error
class AppPortError(AppInternalError):
pass
Поля
msg — короткое человекочитаемое описание на русском, без переменных. Пример: "арендатор не существует", "транзакция уже опубликована", "инициатор не существует".
action — контекст операции, в которой возникла ошибка. Совпадает с ACTION use case-а. Пример: "обновление арендатора", "удаление транзакции", "получение последних версий категорий".
data — безопасный структурированный application-контекст. Это не готовое
тело API-ответа и не transport-контракт.
wrap_error (только AppInternalError) — оригинальное исключение, если ошибка оборачивает техническое.
Конвенция формирования data
Ключи верхнего уровня — английские, в snake_case, отражают тип сущности:
| Тип ошибки |
Ключ верхнего уровня |
Значение |
| Ошибка одного агрегата |
имя сущности в ед. ч.: "tenant", "transaction", "category", "user" |
dict с полями этой сущности |
| Ошибка с участием нескольких сущностей одного типа |
имя во мн. ч.: "categories", "transactions" |
list[dict] |
| Ошибка отдельного значения / поля |
имя поля: "version", "event", "status" |
примитив или dict |
Внутри блока сущности — поля в snake_case, значения — примитивы, stdlib-типы
или неизменяемые application DTO:
# Ошибка одного агрегата
{"tenant": {"tenant_id": UUID(...)}}
# Ошибка с двумя сущностями разных типов
{
"tenant": {"tenant_id": UUID(...)},
"transaction": {"transaction_id": UUID(...)},
}
# Ошибка с коллекцией зависимостей
{
"transaction": {"transaction_id": UUID(...)},
"categories": [
{"category_id": UUID(...)},
{"category_id": UUID(...)},
],
}
# Ошибка отдельного значения
{"event": "wrong_value"}
{"version": 0}
Не помещай в data domain-объекты, VO, технические исключения, stack trace,
секреты и детали адаптера. Для коллекций используй tuple. Исходная причина
хранится только в wrap_error; внешний адаптер сам строит безопасный формат.
Выбор типа ошибки
Выбирай тип только по публичной семантике заданного исхода. Не определяй его
механически по цели, зависимости, инициатору или имени use case. Пустой результат,
отсутствие объекта, конфликт, отказ авторизации и недоступность порта реализуй
ровно так, как задано операцией.
Преобразуй domain-ошибки один раз на границе всего execute явным декоратором.
Базовый класс use case объявляет ACTION: ClassVar[str] без значения, а каждый
конкретный use case задаёт стабильное значение. Instance helper-ы используют
self.ACTION напрямую: не передавай action параметром и не копируй его в
локальную переменную. Общая карта содержит только однозначные соответствия, а
контекстные типы задаются через override-карту класса.
Неизвестный DomainError преобразуй в AppInternalError. Адаптер преобразует
ожидаемую ошибку зависимости в AppPortError; use case придаёт ей публичный смысл
и свой ACTION. Не перехватывай BaseException, CancelledError и публичный
AppError; внутренний AppPortError является отдельным разрешённым случаем.
Правила вызова
data пропускаем, если её нет — не передавать data={} явно.
wrap_error пропускаем, если его нет — не передавать wrap_error=None явно.
AppError напрямую не инстанцируется — только подклассы.
Public API of Module
Что попадает в витрину бизнес-возможности
В application/{command,query}/<business_capability>/__init__.py re-export-ятся
публичные use case и соответствующие Command/Query этой возможности.
| Категория |
Примеры |
| Use case-классы |
UpdateTenantUseCase, GetTenantLastVersionUseCase |
| Соответствующие Command/Query dataclass-ы |
UpdateTenantCommand, GetTenantLastVersionQuery |
Не попадает:
- Helper-методы из
<aggregate>/base.py (приватные, внутреннее использование).
- Базовые классы из
command/base.py / query/base.py — импортируются напрямую.
Шаблон витрины бизнес-возможности
from application.command.user.tenant.delete import (
DeleteTenantCommand,
DeleteTenantUseCase,
)
from application.command.user.tenant.restore import (
RestoreTenantCommand,
RestoreTenantUseCase,
)
from application.command.user.tenant.update import (
UpdateTenantCommand,
UpdateTenantUseCase,
)
__all__ = [
"DeleteTenantCommand",
"DeleteTenantUseCase",
"RestoreTenantCommand",
"RestoreTenantUseCase",
"UpdateTenantCommand",
"UpdateTenantUseCase",
]
__all__ строго в алфавитном порядке.
port/repository/<aggregate>.py и публичная витрина
from application.port.repository.tenant import (
TenantEvent,
TenantOutboxRepository,
TenantReadRepository,
TenantVersionDTO,
TenantVersionRepository,
TenantRepositories,
)
__all__ = [
"TenantEvent",
"TenantOutboxRepository",
"TenantReadRepository",
"TenantRepositories",
"TenantVersionDTO",
"TenantVersionRepository",
# ...
]
TenantRepositories объявляй в repository/tenant.py рядом с интерфейсами своего агрегата. В repository/__init__.py оставляй только re-export и алфавитный __all__.
Top-level application/__init__.py — пустой
Не делай re-export на верхний уровень. Use case импортируй через витрину
бизнес-возможности, DTO, ошибки и базовые классы — прямыми путями.
Правила импорта
| Где |
Откуда импортируем |
Из application/command/<business_capability>/.../<action>.py |
Прямые пути к DTO, ошибкам, портам и domain |
Из application/query/<business_capability>/.../<action>.py |
Аналогично |
| Из входящего адаптера |
Витрина бизнес-возможности |
Из infrastructure/ (для имплементации портов) |
Прямые пути: from application.port.repository.tenant import TenantReadRepository |
Anti-patterns (сводный чеклист)
Утечка слоёв
- Импорт из
infrastructure/ или presentation/ в application/.
- Импорт внешних библиотек в
application/.
- Domain VO/entity в полях команд/запросов.
- Domain VO/entity в return type
execute.
- Domain VO/entity в значениях
data ошибки.
- Re-export use case-ов из верхнеуровневых
__init__.py (только агрегатный уровень).
Транзакционные границы
- Инъекция или хранение экземпляра UoW вместо stateless-фабрики.
- Фабрика, возвращающая ранее использованный UoW.
- Вложенный
async with self._uow_factory() в одном execute.
- Доступ к репозиториям через поле use case вместо локального
uow.
- Несколько транзакций для одного набора согласованных изменений.
- UoW в read-only/stateless-сценарии без транзакционной потребности.
- Helper-метод, работающий с репозиториями, без
uow-параметра.
- Хранение
uow в state объекта.
- Ручной
commit()/rollback() в use case или подавление исключения в __aexit__.
- Использование репозитория после выхода из UoW.
Авторизация
- Авторизация, initiator или role-check, добавленные только по метке user/system.
- Загрузка агрегатов до проверки роли инициатора.
- Глобальная проверка роли без последующего per-aggregate authorization-сервиса (для user-команды над конкретным агрегатом).
_load_initiator вне async with — нужен открытый uow.
Команды, запросы, DTO
- Команда, запрос или структурный DTO без
@dataclass(slots=True, frozen=True);
DTO-перечисление реализуется через стандартный Enum.
- Изменяемые
list/dict внутри frozen DTO; используй tuple и вложенные DTO.
- Поля команд/запросов: VO, domain entity, response-DTO, типы presentation/infrastructure.
- Конверсия примитивов в VO внутри команды (
__post_init__ и т.п.) — конверсия в use case.
execute с двумя+ аргументами — только 0 или 1.
- DTO с методами поведения (валидации, вычисления) — только
from_* фабрики.
- Domain entity/VO в публичном application DTO.
to_dict / to_json / model_dump в DTO — сериализация это presentation.
- Использование
None одновременно как «не передано» и «очистить»; применяй UNSET.
- Одинаковая структурная проверка общего input DTO, скопированная в несколько
use case-ов вместо единственного helper базового класса.
- Неявный глобальный validation pipeline: требуемый helper вызывай явно до I/O
в каждом use case.
- Helper, который одновременно загружает несколько объектов, авторизует операцию
и возвращает tuple; разделяй загрузчики и проверку полномочий.
- Флаг или переданный callback/policy, меняющий ответственность helper-а.
_save, скрывающий от execute сохранение состояния, версии, outbox и вызов
mark_persisted; показывай эти шаги оркестрации явно.
Ports
- Реализация в
port/ — реализации в infrastructure/.
- Беспричинное разбиение
port/repository/<aggregate>.py на мелкие файлы.
- Изменяемая
<Aggregate>Repositories; группа должна быть frozen=True.
next_id() в repository-порте; используй отдельный ID provider.
- Вызов одного и того же порта в цикле вместо требуемого batch-контракта.
Ошибки
- f-строки в
msg.
data={} явным пустым литералом — параметр опускается.
- VO/entity в значениях
data — развёрнуто в примитивы.
- Повторное оборачивание публичного
AppError; AppPortError преобразуется отдельно.
- Пропуск
DomainError или AppPortError во входящий адаптер.
- Перехват
BaseException или asyncio.CancelledError.
- Автоматическое превращение любого нового
DomainError в публичный исход без
защитного fallback в AppInternalError и без контекста ACTION.
- Возврат
AppError | None из порта вместо успешного результата или исключения.
- Пустой
action в ошибке адаптера; адаптер формирует контекст сразу.
- Прямой
raise AppError(...) — только подклассы.
- Отсутствие отдельного типа для публично различимого ошибочного
application-исхода.
- Исключение для ожидаемого результата вроде отсутствия работы вместо заданного
варианта DTO-перечисления.
- Бизнес-логика в
execute, минуя domain.
Outbox / publisher
event_publisher.publish прямо из обычной command use case (нарушение outbox-паттерна).
- Publisher use case с командой на вход.
- DB-транзакция на время сетевой публикации без согласованного изменения состояния.
- Отметка
published до полного успеха batch или частичная отметка при ошибке.
- Формирование broker payload/subject в application вместо infrastructure-адаптера.
- Точки version/outbox-сохранения, отсутствующие в контракте операции.
- Передача изменяемого агрегата и отдельных
change/editor_id вместо
самодостаточного immutable version DTO.
- Outbox DTO, которому адаптеру не хватает данных для сохранения записи.
Naming и структура
- Имена use case-ов с
-ion / -ing / agent-noun — только verb-first (CreateTenant, GetTenantLastVersion).
- Команда без суффикса
Command, запрос без суффикса Query.
- Несколько use case-ов в одном файле.
- Имя файла, не совпадающее с действием.
- Техническое имя DTO (
OutputDTO, PortDTO, ResponseDTO) вместо имени по смыслу данных.
- Команда/запрос с
<Aggregate><Action> ordering вместо verb-first.
- Группировка растущего дерева только по агрегатам или transport-адаптерам.
- Каталог
application/scenario/ и вызов одного use case из другого.
- Динамический или дублирующийся
action; используй ClassVar[str] ACTION.
- Command, Query, агрегат, локальный UoW или результат, сохранённые в
self.
- Неявное кеширование результата в use case вместо отдельного cache-порта.
- Пустой leaf-пакет с
__init__.py или __all__ = [], оставшийся после удаления
или переноса последней операции.
References
references/use_cases.md — анатомия use case-а: базовые классы, конструктор, шаблон execute с фазами, initiator + role, per-aggregate authorization, транзакционные границы UoW, обработка ошибок.
references/commands_and_queries.md — формы команд и запросов, допустимые типы полей, конверсия в VO, return-type правило, контракт execute, command vs query, helper _filtering_data.
references/dtos.md — входные, выходные и внутренние port DTO, допустимые типы и семантическое именование.
references/ports.md — порты: UnitOfWork, EventPublisher, repository-интерфейсы (Read, Version, Outbox, Subscription), группы <Aggregate>Repositories, импорты в port/.
references/outbox_publisher.md — outbox pattern: зачем, форма publisher use case, шаблон, отличия от обычной команды, запрет публикации из команды.
references/checklists.md — пошаговые чек-листы добавления команды, запроса, DTO, портов нового агрегата, publisher use case-а.
1---2name: python-cqrs-application-layer-writing3description: Используй при реализации или правке прикладного слоя в Python-проекте по гексагональной архитектуре с CQRS. Триггеры — добавление или изменение заданного use case, команды, запроса, DTO, порта, UnitOfWork, преобразования ошибок и механизма version/outbox в `application/`. Не применять для определения требований и слоёв `domain/`, `infrastructure/`, `presentation/`.4---56# Python CQRS Application Layer Writing78Общие правила оформления Python-кода брать из `$python-code-style-writing` и не9дублировать здесь; этот скил определяет только контракты application-слоя.1011## Quick Start12131. Извлеки из задачи точные команды, запросы, входы, результаты, порты, порядок шагов, авторизацию, границы согласованности и публичные исходы. Не дополняй контракт типовыми элементами.142. Размести операцию по бизнес-возможности; `user`/`system` используй только при заданном различии.153. Объяви Command, Query и структурные публичные DTO как глубоко неизменяемые16 `@dataclass(slots=True, frozen=True)`: коллекции — `tuple`, вложения — другие17 неизменяемые DTO. Заданное DTO-перечисление ожидаемых результатов реализуй18 через стандартный `Enum`, без Pydantic и transport-значений.194. Для partial update различай `UNSET`, `None` и переданное значение. Не используй truthiness вместо проверки контракта.205. Валидируй структурные ограничения входа в use case до I/O. Повторяемую21 проверку общего input DTO реализуй один раз в helper базового use case, но22 вызывай явно в каждой операции, которой она нужна. DTO оставляй без23 поведения и валидации.246. Преобразуй вход в domain VO до транзакции, если преобразованию не нужен I/O.257. Инжектируй только требуемые порты. Для заданной атомарной группы используй обязательную фабрику нового UoW; независимый порт передавай напрямую.268. Для нового агрегата получай техническое значение ID через типизированный27 исходящий порт, оборачивай его в доменный VO и передавай фабрике. Не требуй28 новый ID во входной команде без заданной внешней семантики.299. Реализуй только заданную авторизацию и порядок доменных вызовов. Не добавляй инициатора, роли и policy по шаблону.3010. Оборачивай `execute` явным application-декоратором преобразования `DomainError`: стабильные типы сопоставляй общей картой, operation-specific типы — override-картой use case, неизвестный отказ — в `AppInternalError`.3111. Техническую ошибку адаптер преобразует в `AppPortError`, а use case — в публичный исход своей операции.3212. Формируй самодостаточные неизменяемые version/outbox DTO в заданных точках33 сохранения. Для закрытого объединения конкретных DTO обязательно объявляй34 явный `TypeAlias` и используй его в outbox/publisher-портах; не реализуй event35 sourcing и не вводи enum ресурсов только для dispatch.3613. Храни в use case только зависимости, а данные вызова — в локальных переменных. Не кешируй результаты в экземпляре.3714. Оставляй в `execute` явную оркестрацию преобразования, загрузки,38 авторизации, доменной операции и сохранения. Каждый helper выполняет только39 один шаг и не возвращает кортеж разнородных загруженных объектов.4015. Не создавай application-сценарии и не вызывай один use case из другого. Реализуй однозначно заданные части без придумывания семантики.4116. Не логируй в application-слое, включая use case, UoW и реализации портов,42 расположенные в этом слое. Сохраняй безопасный контекст в типизированных43 результатах и ошибках.4445## When to Apply4647### Триггеры активации4849**По расположению файлов** — любая правка/создание в `application/`:50- `application/command/<business_capability>/{user,system}/<action>.py`51- `application/query/<business_capability>/{user,system}/<action>.py`52- `application/command/base.py`, `application/query/base.py`53- `application/dto/<aggregate>.py`, `application/dto/paginator.py`54- `application/port/unit_of_work.py`, `application/port/event_publisher.py`55- `application/port/repository/__init__.py`, `application/port/repository/<aggregate>.py`56- `application/error.py`5758**По концепциям, упомянутым пользователем:**59- CQRS-термины: «use case», «сценарий», «command», «команда» в смысле прикладного действия, «query», «запрос».60- DTO, маппинг доменного объекта в DTO.61- Unit of Work, UoW, граница транзакции на уровне сценария.62- Порт, port, адаптер; EventPublisher; outbox-публикация.63- Иерархия прикладных ошибок (`AppError`, `AppNotFoundError`, `AppInvalidDataError`, `AppInternalError`).64- Авторизация инициатора, role check.65- Прикладной слой, application layer.6667**По типам задач:**68- Добавление нового use case (команды или запроса).69- Добавление нового DTO.70- Расширение `UnitOfWork` под новый агрегат.71- Добавление нового интерфейса репозитория или новой группы (`outbox`, `subscription`).72- Добавление события в outbox + соответствующий publisher use case.73- Расширение иерархии прикладных ошибок.74- Правка списочных запросов (filter helper, paginator).75- Добавление источника идентификаторов, часов, кеша или другого исходящего порта.7677### Анти-триггеры7879Скил не активируется, когда правка относится к слоям `domain/`, `infrastructure/` или `presentation/` — для редактирования каждого из этих слоёв используется свой скил:80- `domain/` — доменная модель: агрегаты, value objects, фабрики, доменные сервисы, абстрактные domain-интерфейсы.81- `infrastructure/` — реализации портов: репозитории к БД, адаптеры к брокеру сообщений, миграции схемы БД.82- `presentation/` — точки входа в систему: HTTP-эндпоинты, фоновые worker-ы, схемы валидации входящих данных.83- Составные сценарии и pipeline-описания: отдельные классы сценариев в84 `application/` не создаются, один use case из другого не вызывается.8586Также не активируется на косметические правки внутри `application/` (только docstrings, переименование без изменения контракта, импорт-рефакторинг).8788### Именование структуры8990- Предпочитай единственное число в каталогах и файлах: `command`, `query`, `port`, `repository`, `error.py`.91- Сохраняй уже принятую единообразную конвенцию проекта, если пользователь не согласовал переименование.92- Не включай массовое переименование в функциональную задачу без явного согласия.93- Все пути далее — иллюстрации; подставляй согласованную форму единственного или множественного числа последовательно.9495### Предусловия9697- Гексагональная архитектура с выделенным `application/`-слоем.98- CQRS: команды и запросы разнесены по разным деревьям.99- `application/` импортирует только из `domain/` и стандартной библиотеки. Запрещены импорты из `infrastructure/`, `presentation/`. Внешние библиотеки (web-фреймворки, драйверы БД, клиенты брокеров и т.п.) не используются.100- Состав и семантика application-операций заданы входными требованиями. Скил101 выбирает Python-реализацию, но не проектирует новые поля, исходы и шаги.102- Прикладная авторизация использует только заданные сведения и domain-контракты.103 Аутентификация и transport context остаются во входящем адаптере.104105## Package Structure106107```108application/109├── __init__.py ← пустой110├── error.py ← иерархия AppError111│112├── dto/113│ ├── __init__.py ← пустой114│ ├── unset.py ← UNSET для partial update115│ ├── paginator.py ← LimitOffsetPaginator и общие пагинаторы116│ └── <meaning>.py ← публичные application DTO117├── error_mapping/118│ └── domain.py ← единая граница domain-ошибок119│120├── port/121│ ├── __init__.py ← пустой122│ ├── unit_of_work.py ← UnitOfWork + обязательная UnitOfWorkFactory123│ ├── identifier.py ← только требуемые типизированные источники ID124│ ├── event_publisher.py ← EventPublisher ABC + TypeAlias событий125│ ├── dto/ ← самодостаточные port DTO126│ │ └── <meaning>.py127│ └── repository/128│ ├── __init__.py ← только re-export + __all__129│ └── <aggregate>.py ← интерфейсы + <Aggregate>Repositories130│131├── command/132│ ├── __init__.py ← пустой133│ ├── base.py ← только реально общие helpers134│ └── <business_capability>/135│ ├── __init__.py ← публичная витрина возможности136│ ├── user/ ← только при заданном разделении137│ │ └── <action>.py138│ └── system/ ← только при заданном разделении139│ └── <action>.py140│141└── query/142 ├── __init__.py ← пустой143 ├── base.py ← только реально общие helpers144 └── <business_capability>/145 ├── __init__.py ← публичная витрина возможности146 ├── user/147 │ └── <action>.py148 └── system/149 └── <action>.py150```151152Для малого набора операций сохраняй принятую плоскую структуру. Группируй по153бизнес-возможности при росте: более 12 файлов на уровне, минимум две устойчивые154группы с двумя операциями, повторяющиеся префиксы, необходимость индекса или155очевидное скорое достижение этих условий. Не перемещай существующие файлы156автоматически. Уровень bounded context добавляй только при явно выделенных157нескольких контекстах и устойчивой группировке возможностей.158159### Назначение узлов160161Ветки `user` и `system` одинаково применяются к командам и запросам:162163- `user` и `system` отражают заданный источник операции, но сами по себе не164 добавляют `initiator_id`, роль или policy. Включай только сведения и проверки165 конкретного контракта.166167Эта классификация не определяет доступность операции через HTTP или брокер и не168является заменой модификаторам видимости.169170| Узел | Что внутри | Канон |171|---|---|---|172| `error.py` | `AppError` + `AppNotFoundError`, `AppInvalidDataError`, `AppInternalError` | один файл, не дробится |173| `dto/<meaning>.py` | Публичные application DTO, названные по смыслу данных | по связному представлению |174| `dto/unset.py` | Общий `UNSET` для partial update | один файл |175| `dto/paginator.py` | общие пагинаторы (`LimitOffsetPaginator`) | один на проект |176| `port/unit_of_work.py` | `UnitOfWork` ABC + обязательная фабрика нового экземпляра | один файл |177| `port/identifier.py` | типизированные источники новых ID | только при потребности |178| `port/event_publisher.py` | `EventPublisher` ABC + `TypeAlias` допустимых event DTO | один файл |179| `port/dto/<meaning>.py` | Самодостаточные внутренние DTO портов | создаётся по необходимости |180| `port/repository/__init__.py` | Только re-export и алфавитный `__all__` | публичная витрина |181| `port/repository/<aggregate>.py` | Repository-интерфейсы и `<Aggregate>Repositories` | по одному файлу на агрегат |182| `command/base.py` / `query/base.py` | базовые классы use case-ов (если приняты в проекте) | пример конвенции, не предписание |183| `command/<business_capability>/.../<action>.py` | один use case = один файл | группировка по бизнес-возможности |184| `query/<business_capability>/.../<action>.py` | dataclass-запрос + use case-класс | то же |185| `error_mapping/domain.py` | Декоратор границы `execute`, стабильная карта и operation-specific overrides | один файл |186187### Конвенции по `__init__.py`188189Не создавай пустые leaf-пакеты возможностей или источников операций и не190оставляй `__all__ = []` как заглушку. При удалении или переносе последней191операции удали ставшие пустыми `__init__.py` и каталог. Ветки `user` и `system`192существуют только пока содержат хотя бы одну актуальную операцию.193194| Уровень | Состояние | Содержимое |195|---|---|---|196| `application/__init__.py` | пустой | — |197| `application/{command,query,dto,port}/__init__.py` | пустой | — |198| `application/{command,query}/<business_capability>/__init__.py` | публичная витрина | операции возможности |199| **`application/command/<business_capability>/__init__.py`** | **не пустой** | re-export публичных use case и Command возможности |200| **`application/query/<business_capability>/__init__.py`** | **не пустой** | re-export публичных use case и Query возможности |201| `application/dto/__init__.py` | пустой | DTO импортируются напрямую из `application.dto.<aggregate>` |202| `application/port/__init__.py` | пустой | — |203| **`application/port/repository/__init__.py`** | **не пустой** | только re-export контрактов и алфавитный `__all__` |204205Входящие адаптеры импортируют операции через витрину бизнес-возможности.206Изнутри application используй прямые пути.207208### Naming convention для use case-ов (CQRS verb-first)209210**Команды — императивный verb + аггрегат:**211212| Файл | Command | UseCase |213|---|---|---|214| `create.py` | `Create<Aggregate>Command` | `Create<Aggregate>UseCase` |215| `update.py` | `Update<Aggregate>Command` | `Update<Aggregate>UseCase` |216| `delete.py` | `Delete<Aggregate>Command` | `Delete<Aggregate>UseCase` |217| `restore.py` | `Restore<Aggregate>Command` | `Restore<Aggregate>UseCase` |218| `publish.py` | — (нет входа) | `Publish<Aggregate>VersionsUseCase` |219| `<verb>_<object>.py` (доменный) | `<Verb><Aggregate><Object>Command` | `<Verb><Aggregate><Object>UseCase` |220221**Запросы — `Get` / `List` + описание возвращаемого результата:**222223| Файл | Query | UseCase |224|---|---|---|225| `retrieve_last_version.py` | `Get<Aggregate>LastVersionQuery` | `Get<Aggregate>LastVersionUseCase` |226| `retrieve_version.py` | `Get<Aggregate>VersionQuery` | `Get<Aggregate>VersionUseCase` |227| `list_last_versions.py` | `List<Aggregate>LastVersionsQuery` | `List<Aggregate>LastVersionsUseCase` |228| `list_versions.py` | `List<Aggregate>VersionsQuery` | `List<Aggregate>VersionsUseCase` |229230Имена файлов следуют столбцу «Файл» — это часть структуры слоя, не имя класса.231232Тип сохранённого изменения называй по смыслу (`ProjectChange`, `TenantChange`) и233размещай рядом с version DTO. Это application-метаданные снимка, не доменное234событие и не transport message.235236### Чего в layout-е нет и почему237238- `system` и `user` обозначают источник сценария, а не публичность Python API или транспортную доступность.239- Ветки `command/{user,system}/` и `query/{user,system}/` создавай только при240 наличии реальной операции; пустые ветки заранее не добавляй и после удаления241 последней операции не сохраняй.242- Не создавай event-sourcing API, `events.py` и коллекцию событий агрегата.243244## Errors Hierarchy245246### Полная иерархия247248```249AppError (msg, action, data)250├── AppNotFoundError — основной ресурс не найден251├── AppInvalidDataError — зависимость/контекст невалиден или отсутствует252├── AppForbiddenError — операция запрещена прикладной политикой253├── <PublicOutcomeError> — различимый публичный исход операции254├── AppInternalError — непредусмотренный внутренний исход (+ wrap_error)255└── AppPortError — внутренняя ошибка исходящего порта (+ wrap_error)256```257258Иерархия живёт в `application/error.py`. Создавай отдельный публичный тип для259каждого ошибочного исхода, который вызывающая сторона должна различать. Ожидаемые260управляющие результаты возвращай заданным DTO и не представляй исключениями. Не261создавай разные типы для одинаковой публичной семантики. `AppPortError` не262пересекает входящую границу: use case всегда преобразует его в публичный263ошибочный исход или `AppInternalError`.264265### Базовый класс266267```python268from typing import Any269270271class AppError(Exception):272 def __init__(273 self,274 msg: str,275 action: str,276 data: dict[str, Any] | None = None,277 *args: object,278 ) -> None:279 super().__init__(msg, *args)280 self.msg = msg281 self.action = action282 self.data = data or {}283284285class AppNotFoundError(AppError):286 pass287288289class AppInvalidDataError(AppError):290 pass291292293class AppInternalError(AppError):294 def __init__(295 self,296 msg: str,297 action: str,298 data: dict[str, Any] | None = None,299 wrap_error: BaseException | None = None,300 *args: object,301 ) -> None:302 super().__init__(msg, action, data, *args)303 self.wrap_error = wrap_error304305306class AppPortError(AppInternalError):307 pass308```309310### Поля311312- **`msg`** — короткое человекочитаемое описание на русском, без переменных. Пример: `"арендатор не существует"`, `"транзакция уже опубликована"`, `"инициатор не существует"`.313- **`action`** — контекст операции, в которой возникла ошибка. Совпадает с `ACTION` use case-а. Пример: `"обновление арендатора"`, `"удаление транзакции"`, `"получение последних версий категорий"`.314- **`data`** — безопасный структурированный application-контекст. Это не готовое315 тело API-ответа и не transport-контракт.316- **`wrap_error`** (только `AppInternalError`) — оригинальное исключение, если ошибка оборачивает техническое.317318### Конвенция формирования `data`319320**Ключи верхнего уровня — английские, в snake_case, отражают тип сущности:**321322| Тип ошибки | Ключ верхнего уровня | Значение |323|---|---|---|324| Ошибка одного агрегата | имя сущности в ед. ч.: `"tenant"`, `"transaction"`, `"category"`, `"user"` | `dict` с полями этой сущности |325| Ошибка с участием нескольких сущностей одного типа | имя во мн. ч.: `"categories"`, `"transactions"` | `list[dict]` |326| Ошибка отдельного значения / поля | имя поля: `"version"`, `"event"`, `"status"` | примитив или `dict` |327328**Внутри блока сущности — поля в snake_case, значения — примитивы, stdlib-типы329или неизменяемые application DTO:**330331```python332# Ошибка одного агрегата333{"tenant": {"tenant_id": UUID(...)}}334335# Ошибка с двумя сущностями разных типов336{337 "tenant": {"tenant_id": UUID(...)},338 "transaction": {"transaction_id": UUID(...)},339}340341# Ошибка с коллекцией зависимостей342{343 "transaction": {"transaction_id": UUID(...)},344 "categories": [345 {"category_id": UUID(...)},346 {"category_id": UUID(...)},347 ],348}349350# Ошибка отдельного значения351{"event": "wrong_value"}352{"version": 0}353```354355Не помещай в `data` domain-объекты, VO, технические исключения, stack trace,356секреты и детали адаптера. Для коллекций используй `tuple`. Исходная причина357хранится только в `wrap_error`; внешний адаптер сам строит безопасный формат.358359### Выбор типа ошибки360361Выбирай тип только по публичной семантике заданного исхода. Не определяй его362механически по цели, зависимости, инициатору или имени use case. Пустой результат,363отсутствие объекта, конфликт, отказ авторизации и недоступность порта реализуй364ровно так, как задано операцией.365366Преобразуй domain-ошибки один раз на границе всего `execute` явным декоратором.367Базовый класс use case объявляет `ACTION: ClassVar[str]` без значения, а каждый368конкретный use case задаёт стабильное значение. Instance helper-ы используют369`self.ACTION` напрямую: не передавай `action` параметром и не копируй его в370локальную переменную. Общая карта содержит только однозначные соответствия, а371контекстные типы задаются через override-карту класса.372Неизвестный `DomainError` преобразуй в `AppInternalError`. Адаптер преобразует373ожидаемую ошибку зависимости в `AppPortError`; use case придаёт ей публичный смысл374и свой `ACTION`. Не перехватывай `BaseException`, `CancelledError` и публичный375`AppError`; внутренний `AppPortError` является отдельным разрешённым случаем.376377### Правила вызова378379- **`data` пропускаем, если её нет** — не передавать `data={}` явно.380- **`wrap_error` пропускаем, если его нет** — не передавать `wrap_error=None` явно.381- **`AppError` напрямую не инстанцируется** — только подклассы.382383## Public API of Module384385### Что попадает в витрину бизнес-возможности386387В `application/{command,query}/<business_capability>/__init__.py` re-export-ятся388публичные use case и соответствующие Command/Query этой возможности.389390| Категория | Примеры |391|---|---|392| Use case-классы | `UpdateTenantUseCase`, `GetTenantLastVersionUseCase` |393| Соответствующие Command/Query dataclass-ы | `UpdateTenantCommand`, `GetTenantLastVersionQuery` |394395**Не попадает:**396- Helper-методы из `<aggregate>/base.py` (приватные, внутреннее использование).397- Базовые классы из `command/base.py` / `query/base.py` — импортируются напрямую.398399### Шаблон витрины бизнес-возможности400401```python402from application.command.user.tenant.delete import (403 DeleteTenantCommand,404 DeleteTenantUseCase,405)406from application.command.user.tenant.restore import (407 RestoreTenantCommand,408 RestoreTenantUseCase,409)410from application.command.user.tenant.update import (411 UpdateTenantCommand,412 UpdateTenantUseCase,413)414415__all__ = [416 "DeleteTenantCommand",417 "DeleteTenantUseCase",418 "RestoreTenantCommand",419 "RestoreTenantUseCase",420 "UpdateTenantCommand",421 "UpdateTenantUseCase",422]423```424425**`__all__` строго в алфавитном порядке.**426427### `port/repository/<aggregate>.py` и публичная витрина428429```python430from application.port.repository.tenant import (431 TenantEvent,432 TenantOutboxRepository,433 TenantReadRepository,434 TenantVersionDTO,435 TenantVersionRepository,436 TenantRepositories,437)438439__all__ = [440 "TenantEvent",441 "TenantOutboxRepository",442 "TenantReadRepository",443 "TenantRepositories",444 "TenantVersionDTO",445 "TenantVersionRepository",446 # ...447]448```449450`TenantRepositories` объявляй в `repository/tenant.py` рядом с интерфейсами своего агрегата. В `repository/__init__.py` оставляй только re-export и алфавитный `__all__`.451452### Top-level `application/__init__.py` — пустой453454Не делай re-export на верхний уровень. Use case импортируй через витрину455бизнес-возможности, DTO, ошибки и базовые классы — прямыми путями.456457### Правила импорта458459| Где | Откуда импортируем |460|---|---|461| Из `application/command/<business_capability>/.../<action>.py` | Прямые пути к DTO, ошибкам, портам и domain |462| Из `application/query/<business_capability>/.../<action>.py` | Аналогично |463| Из входящего адаптера | Витрина бизнес-возможности |464| Из `infrastructure/` (для имплементации портов) | Прямые пути: `from application.port.repository.tenant import TenantReadRepository` |465466## Anti-patterns (сводный чеклист)467468### Утечка слоёв469- Импорт из `infrastructure/` или `presentation/` в `application/`.470- Импорт внешних библиотек в `application/`.471- Domain VO/entity в полях команд/запросов.472- Domain VO/entity в return type `execute`.473- Domain VO/entity в значениях `data` ошибки.474- Re-export use case-ов из верхнеуровневых `__init__.py` (только агрегатный уровень).475476### Транзакционные границы477- Инъекция или хранение экземпляра UoW вместо stateless-фабрики.478- Фабрика, возвращающая ранее использованный UoW.479- Вложенный `async with self._uow_factory()` в одном `execute`.480- Доступ к репозиториям через поле use case вместо локального `uow`.481- Несколько транзакций для одного набора согласованных изменений.482- UoW в read-only/stateless-сценарии без транзакционной потребности.483- Helper-метод, работающий с репозиториями, без `uow`-параметра.484- Хранение `uow` в state объекта.485- Ручной `commit()`/`rollback()` в use case или подавление исключения в `__aexit__`.486- Использование репозитория после выхода из UoW.487488### Авторизация489- Авторизация, initiator или role-check, добавленные только по метке user/system.490- Загрузка агрегатов **до** проверки роли инициатора.491- Глобальная проверка роли без последующего per-aggregate authorization-сервиса (для user-команды над конкретным агрегатом).492- `_load_initiator` вне `async with` — нужен открытый `uow`.493494### Команды, запросы, DTO495- Команда, запрос или структурный DTO без `@dataclass(slots=True, frozen=True)`;496 DTO-перечисление реализуется через стандартный `Enum`.497- Изменяемые `list`/`dict` внутри frozen DTO; используй `tuple` и вложенные DTO.498- Поля команд/запросов: VO, domain entity, response-DTO, типы presentation/infrastructure.499- Конверсия примитивов в VO внутри команды (`__post_init__` и т.п.) — конверсия в use case.500- `execute` с двумя+ аргументами — только 0 или 1.501- DTO с методами поведения (валидации, вычисления) — только `from_*` фабрики.502- Domain entity/VO в публичном application DTO.503- `to_dict` / `to_json` / `model_dump` в DTO — сериализация это presentation.504- Использование `None` одновременно как «не передано» и «очистить»; применяй `UNSET`.505- Одинаковая структурная проверка общего input DTO, скопированная в несколько506 use case-ов вместо единственного helper базового класса.507- Неявный глобальный validation pipeline: требуемый helper вызывай явно до I/O508 в каждом use case.509- Helper, который одновременно загружает несколько объектов, авторизует операцию510 и возвращает tuple; разделяй загрузчики и проверку полномочий.511- Флаг или переданный callback/policy, меняющий ответственность helper-а.512- `_save`, скрывающий от `execute` сохранение состояния, версии, outbox и вызов513 `mark_persisted`; показывай эти шаги оркестрации явно.514515### Ports516- Реализация в `port/` — реализации в `infrastructure/`.517- Беспричинное разбиение `port/repository/<aggregate>.py` на мелкие файлы.518- Изменяемая `<Aggregate>Repositories`; группа должна быть `frozen=True`.519- `next_id()` в repository-порте; используй отдельный ID provider.520- Вызов одного и того же порта в цикле вместо требуемого batch-контракта.521522### Ошибки523- f-строки в `msg`.524- `data={}` явным пустым литералом — параметр опускается.525- VO/entity в значениях `data` — развёрнуто в примитивы.526- Повторное оборачивание публичного `AppError`; `AppPortError` преобразуется отдельно.527- Пропуск `DomainError` или `AppPortError` во входящий адаптер.528- Перехват `BaseException` или `asyncio.CancelledError`.529- Автоматическое превращение любого нового `DomainError` в публичный исход без530 защитного fallback в `AppInternalError` и без контекста `ACTION`.531- Возврат `AppError | None` из порта вместо успешного результата или исключения.532- Пустой `action` в ошибке адаптера; адаптер формирует контекст сразу.533- Прямой `raise AppError(...)` — только подклассы.534- Отсутствие отдельного типа для публично различимого ошибочного535 application-исхода.536- Исключение для ожидаемого результата вроде отсутствия работы вместо заданного537 варианта DTO-перечисления.538- Бизнес-логика в `execute`, минуя domain.539540### Outbox / publisher541- `event_publisher.publish` прямо из обычной command use case (нарушение outbox-паттерна).542- Publisher use case с командой на вход.543- DB-транзакция на время сетевой публикации без согласованного изменения состояния.544- Отметка `published` до полного успеха batch или частичная отметка при ошибке.545- Формирование broker payload/subject в application вместо infrastructure-адаптера.546- Точки version/outbox-сохранения, отсутствующие в контракте операции.547- Передача изменяемого агрегата и отдельных `change`/`editor_id` вместо548 самодостаточного immutable version DTO.549- Outbox DTO, которому адаптеру не хватает данных для сохранения записи.550551### Naming и структура552- Имена use case-ов с `-ion` / `-ing` / agent-noun — только verb-first (`CreateTenant`, `GetTenantLastVersion`).553- Команда без суффикса `Command`, запрос без суффикса `Query`.554- Несколько use case-ов в одном файле.555- Имя файла, не совпадающее с действием.556- Техническое имя DTO (`OutputDTO`, `PortDTO`, `ResponseDTO`) вместо имени по смыслу данных.557- Команда/запрос с `<Aggregate><Action>` ordering вместо verb-first.558- Группировка растущего дерева только по агрегатам или transport-адаптерам.559- Каталог `application/scenario/` и вызов одного use case из другого.560- Динамический или дублирующийся `action`; используй `ClassVar[str] ACTION`.561- Command, Query, агрегат, локальный UoW или результат, сохранённые в `self`.562- Неявное кеширование результата в use case вместо отдельного cache-порта.563- Пустой leaf-пакет с `__init__.py` или `__all__ = []`, оставшийся после удаления564 или переноса последней операции.565566## References567568- **`references/use_cases.md`** — анатомия use case-а: базовые классы, конструктор, шаблон `execute` с фазами, initiator + role, per-aggregate authorization, транзакционные границы UoW, обработка ошибок.569- **`references/commands_and_queries.md`** — формы команд и запросов, допустимые типы полей, конверсия в VO, return-type правило, контракт `execute`, command vs query, helper `_filtering_data`.570- **`references/dtos.md`** — входные, выходные и внутренние port DTO, допустимые типы и семантическое именование.571- **`references/ports.md`** — порты: `UnitOfWork`, `EventPublisher`, repository-интерфейсы (`Read`, `Version`, `Outbox`, `Subscription`), группы `<Aggregate>Repositories`, импорты в `port/`.572- **`references/outbox_publisher.md`** — outbox pattern: зачем, форма publisher use case, шаблон, отличия от обычной команды, запрет публикации из команды.573- **`references/checklists.md`** — пошаговые чек-листы добавления команды, запроса, DTO, портов нового агрегата, publisher use case-а.