Python DDD Domain Layer Writing
Общие правила оформления Python-кода брать из $python-code-style-writing и не
дублировать здесь; этот скил определяет только контракты domain-слоя.
Quick Start
- Выдели из задачи точный состав агрегатов, сущностей, проекций, полей, операций, состояний, инвариантов и отказов. Не дополняй его типовыми элементами.
- Создай для каждого заданного поля отдельный неизменяемый объект-значение. Внутри доменной модели не используй сырые примитивы.
- Создай поддиректорию
domain/<name>/ и последовательно: value_object.py → aggregate.py (или projection.py) → обязательный factory.py → только требуемые repository.py и service.py.
- Разреши создание и восстановление агрегата или проекции только через фабрику.
new() и restore() принимают готовые VO; идентификатор всегда передаёт вызывающий слой.
- Реализуй только заданные состояния, переходы, инварианты, поведение, семантику повторов и проверки версий.
- После всех проверок выполни мутацию агрегата и увеличь версию ровно один раз. При отказе состояние и версия не меняются.
- Проекция принимает заданную версию извне и самостоятельно её не увеличивает. Не добавляй ID-parity без явного требования.
- Различимые доменные бизнес-отказы представь отдельными типами.
- Не создавай коллекцию доменных событий: тип сохранённого изменения, version storage, outbox и публикация принадлежат внешним слоям.
- Обнови витрину
__init__.py поддиректории.
- Внутри
domain/ запрещены импорты из application/, infrastructure/, presentation/ и внешних библиотек.
- Все приватные поля начинаются с
_ и экспонируются без возможности внешней мутации.
- Реализуй однозначно заданные части; не придумывай недостающую бизнес-семантику для остальных.
When to Apply
Триггеры активации
По расположению файлов — любая правка/создание в domain/:
domain/<aggregate>/aggregate.py, domain/<aggregate>/factory.py, domain/<aggregate>/service.py, domain/<aggregate>/repository.py, domain/<aggregate>/value_object.py
domain/<projection>/projection.py и связанные
- Общие файлы:
domain/value_object.py, domain/error.py, domain/aggregate.py, domain/projection.py
По концепциям, упомянутым пользователем:
- DDD-термины: «агрегат», «aggregate», «value object», «VO», «доменная сущность», «entity», «проекция», «projection», «доменный сервис», «domain service», «фабрика», «factory», «репозиторий-интерфейс».
- Бизнес-инварианты, инкапсуляция бизнес-логики, неизменяемость состояния.
- Оптимистичный параллелизм / версионирование агрегата.
- Управление состоянием сущности (
active/deleted/frozen).
- Read-модели, проекции из других bounded context.
По типам задач:
- Добавление нового агрегата или проекции.
- Добавление/изменение VO.
- Добавление/изменение метода поведения агрегата (нового мутатора).
- Добавление доменного сервиса для проверки конкретного бизнес-правила.
- Добавление абстрактного repository-интерфейса в domain.
- Расширение иерархии доменных ошибок.
Анти-триггеры
- Правка
application/ (use cases, commands, queries, DTO, ports).
- Правка
infrastructure/ (psycopg-репозитории, NATS-адаптеры, миграции).
- Правка
presentation/ (FastAPI routers, background workers).
- Импорт-рефакторинг, переименование без изменения структуры.
Предусловия
- Проект следует гексагональной архитектуре с выделенным
domain/-слоем.
- В
domain/ запрещены импорты из application/, infrastructure/, presentation/. Допустимы только импорты внутри domain/ и из стандартной библиотеки.
- Состав и семантика доменной модели заданы входными требованиями. Скил выбирает
способ реализации на Python, но не проектирует новые бизнес-контракты.
Package Structure
domain/
├── __init__.py ← пустой, не делаем re-export верхнего уровня
│
├── value_object.py ← общие VO (Version, DomainObjectName, State)
├── error.py ← иерархия доменных ошибок
├── aggregate.py ← базовые AggregateRoot / AggregateRootWithState
├── projection.py ← базовые Projection / ProjectionWithState
│
├── <aggregate_name>/ ← поддиректория на агрегат
│ ├── __init__.py ← re-export публичного API через __all__
│ ├── aggregate.py
│ ├── value_object.py
│ ├── factory.py
│ ├── repository.py ← только для требуемого доменного поиска
│ └── service.py ← только для заданного межобъектного правила
│
└── <projection_name>/ ← поддиректория на проекцию
├── __init__.py
├── projection.py ← вместо aggregate.py!
├── value_object.py
├── factory.py
├── repository.py ← только для требуемого доменного поиска
└── service.py ← только для заданного межобъектного правила
Содержимое верхнеуровневых файлов
domain/value_object.py — VO, разделяемые между агрегатами/проекциями. Правило: VO попадает сюда, только если используется ≥ 2 разными модулями ИЛИ базовыми классами. Иначе — в domain/<name>/value_object.py.
domain/error.py — единая публичная иерархия доменных ошибок, включая
специализированные типы различимых бизнес-исходов.
domain/aggregate.py — базовые AggregateRoot и AggregateRootWithState. Других классов в этом файле быть не должно.
domain/projection.py — базовые Projection и ProjectionWithState.
Реализация заданного типа
Агрегат:
- Реализуй заданную границу и только перечисленное поведение.
- Изменяй принадлежащие объекты только через корень агрегата.
- После успешной изменяющей операции увеличивай версию ровно один раз.
Проекция:
- Реализуй ровно перечисленные поля и операции локального представления.
- Принимай новую версию извне и не инкрементируй её внутри domain.
- Не принимай transport-модели или application DTO.
- В файле используй
projection.py и базовый Projection / ProjectionWithState.
Правила импортов внутри domain/
Разрешено:
- Импорт из стандартной библиотеки (
uuid, decimal, datetime, enum, dataclasses, typing, abc).
- Импорт между модулями
domain/ (например, from domain.tenant import TenantID в personal_transaction/aggregate.py).
- Импорт из общих файлов (
domain.value_object, domain.error, domain.aggregate, domain.projection).
Запрещено:
- Импорты из
application/, infrastructure/, presentation/.
- Импорты внешних библиотек (никаких
pydantic, psycopg, fastapi).
- Циклические импорты между поддиректориями. При риске цикла — выносим общий VO в
domain/value_object.py.
Именование
- Модуль агрегата/проекции — snake_case в единственном числе (
personal_transaction, не personal_transactions).
- Класс агрегата — PascalCase, существительное в единственном числе (
PersonalTransaction, Tenant).
- ID-VO —
<Aggregate>ID (PersonalTransactionID, не PersonalTransactionId).
- Фабрика —
<Aggregate>Factory.
- Repository-интерфейс —
<Aggregate>ReadRepository.
- Сервис именуется по конкретному бизнес-правилу:
UserUniquenessService, TransactionOwnershipPolicy.
Error Hierarchy
Полная иерархия
DomainError (msg, subject, data)
├── ValueObjectError
│ └── ValueObjectInvalidDataError
└── EntityError
├── EntityInvalidDataError
├── EntityVersionError
├── EntityIdempotentError
├── EntityPolicyError
├── EntityAlreadyExistsError
└── EntityNotFoundError
Вся публичная иерархия живёт в domain/error.py. Для каждого бизнес-исхода,
который вызывающий слой должен отличать от остальных, создавай отдельный тип,
наследуя его от подходящей общей категории. Не создавай разные типы для внутренних
проверок с одинаковым наблюдаемым смыслом.
Базовый класс
from typing import Any
class DomainError(Exception):
def __init__(
self,
msg: str,
subject: str,
data: dict[str, Any] | None = None,
) -> None:
super().__init__(msg)
self.msg = msg
self.subject = subject
self.data = data or {}
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"msg={self.msg!r}, subject={self.subject!r}, data={self.data!r})"
)
class ValueObjectError(DomainError):
pass
class ValueObjectInvalidDataError(ValueObjectError):
pass
class EntityError(DomainError):
pass
class EntityInvalidDataError(EntityError):
pass
class EntityVersionError(EntityError):
pass
class EntityIdempotentError(EntityError):
pass
class EntityPolicyError(EntityError):
pass
class EntityAlreadyExistsError(EntityError):
pass
class EntityNotFoundError(EntityError):
pass
Поля
msg — сообщение об ошибке на языке домена. Пример: "новое состояние идентично текущему", "арендатор удален", "только владелец может работать с категорией".
subject — человекочитаемая метка предмета ошибки на языке домена. Это не имя класса/модуля, а название агрегата/VO/проекции в терминах бизнеса. Для агрегатов и проекций берётся из общего domain_object_name.name, для остальных VO задаётся явно. Примеры: "арендатор", "категория транзакций", "проекция пользователя", "название категории", "версия агрегата".
data — безопасный структурированный контекст ошибки для вызывающего слоя.
Опционально, по умолчанию {}. Domain не логирует ошибки и не определяет их
transport-представление.
Конвенция формирования data
Ключи верхнего уровня — английские, в snake_case, отражают тип сущности:
| Тип ошибки |
Ключ верхнего уровня |
Значение |
| Ошибка одного агрегата |
имя сущности в ед. ч.: "tenant", "transaction", "category", "user" |
dict с полями этой сущности |
| Ошибка с участием нескольких сущностей одного типа |
имя во мн. ч.: "categories", "transactions" |
list[dict] |
| Ошибка VO |
имя поля VO: "version", "name", "transaction_type" |
примитив или dict |
Внутри блока сущности — поля в snake_case (английские), значения — примитивы для JSON:
# Ошибка одного агрегата
{"tenant": {"tenant_id": UUID(...), "state": "active"}}
# Ошибка с двумя агрегатами разных типов
{
"tenant": {"tenant_id": UUID(...)},
"transaction": {"owner_id": UUID(...)},
}
# Ошибка с коллекцией
{
"transaction": {"transaction_id": UUID(...)},
"categories": [
{"category_id": UUID(...), "name": "еда"},
{"category_id": UUID(...), "name": "транспорт"},
],
}
# Ошибка VO
{"version": 0}
{"transaction_type": "wrong_value"}
{"money_amount": {"amount": "-10.50", "currency": "ruble"}}
Правила значений:
UUID оставляем как UUID-объект, не строкой — сериализатор API сам приведёт.
Decimal приводим к str (str(amount)), чтобы избежать потери точности при JSON-сериализации.
datetime оставляем как есть, сериализатор API приведёт.
Enum берём .value (строку), не сам enum.
Семантика подклассов
| Класс |
Когда бросать |
ValueObjectInvalidDataError |
VO получил невалидные данные при конструировании. В __post_init__ или from_str. |
EntityInvalidDataError |
Операция над сущностью невозможна из-за её состояния или входных данных, не подпадающих под идемпотентность/политику. Типичные случаи: попытка изменить удалённую сущность (через _check_state), пустые данные на входе, несовместимые входные данные. |
EntityVersionError |
Нарушение контракта версии сущности или проекции, включая получение устаревшей версии. |
EntityIdempotentError |
Повторный вызов должен завершиться доменным отказом согласно заданной семантике. Не используй его автоматически для каждого равенства значений. |
EntityPolicyError |
Нарушение бизнес-политики, не связанное с состоянием самой сущности (только владелец может, только админ может, доступ запрещён из-за состояния субъекта). |
EntityAlreadyExistsError |
Бизнес-правило уникальности обнаружило уже существующую сущность. |
EntityNotFoundError |
Бизнес-правило требует существования связанной сущности, но она не найдена. |
Правила вызова
data пропускаем, если её нет — не передавать data={} явно.
- В агрегатах используем
_error_data хелпер — он подставляет subject из domain_object_name.name и добавляет ID агрегата в data: raise EntityIdempotentError(**self._error_data(msg="...", data={...})).
- В VO и в сервисах заполняем поля явно (хелпера нет).
Public API of a Module (__init__.py and __all__)
Что попадает в витрину
| Категория |
Примеры |
| Класс агрегата / проекции |
Tenant, User |
| Фабрика |
TenantFactory, UserFactory |
| Все VO агрегата/проекции |
TenantID, TenantState, TenantStatus |
| Repository-интерфейс (если есть) |
TenantReadRepository |
| Domain-сервисы (если есть) |
UserUniquenessService, TransactionOwnershipPolicy |
Не попадает:
- Приватные хелперы (имя с
_).
- Базовые классы из общих файлов (
AggregateRoot, Projection, DomainError) — импортируются напрямую.
Шаблон __init__.py
from domain.<aggregate>.entity import <Aggregate>
from domain.<aggregate>.factory import <Aggregate>Factory
from domain.<aggregate>.repository import <Aggregate>ReadRepository
from domain.<aggregate>.service import <Aggregate>UniquenessService
from domain.<aggregate>.value_object import (
<Aggregate>ID,
<Aggregate>Name,
<Aggregate>State,
)
__all__ = [
"<Aggregate>",
"<Aggregate>Factory",
"<Aggregate>ID",
"<Aggregate>Name",
"<Aggregate>ReadRepository",
"<Aggregate>State",
"<Aggregate>UniquenessService",
]
__all__ строго в алфавитном порядке.
Top-level domain/__init__.py — пустой
Не делаем re-export всех агрегатов на верхний уровень. Импорты на стороне идут через поддиректории.
Правила импорта
| Где |
Откуда импортируем |
Внутри domain/<aggregate>/*.py (тот же модуль) |
Прямые пути: from domain.<aggregate>.entity import ... |
Из domain/<other_aggregate>/*.py |
Витрина: from domain.<other_aggregate> import ... |
Из application/, infrastructure/, presentation/ |
Витрина: from domain.<aggregate> import ... |
Общие файлы (domain/aggregate.py, domain/error.py, ...) |
Прямые пути: from domain.error import ... |
References
references/value_objects.md — три типа VO (Identity, Validated, Enum) + Multi-field, правила иммутабельности, нормализация, валидация.
references/aggregates.md — базовые классы AggregateRoot / AggregateRootWithState, паттерны агрегатов, фабрики агрегатов, особый случай расширенного состояния.
references/projections.md — базовые классы Projection /
ProjectionWithState, операции, фабрики и правила входящей версии.
references/services_and_repositories.md — сервисы конкретных бизнес-правил и необходимые им repository-интерфейсы.
references/testing.md — unit-тесты domain-инвариантов, версии, фабрик, fixtures и покрытие.
references/checklists.md — пошаговые чек-листы добавления агрегата и проекции.
1---2name: python-ddd-domain-layer-writing3description: Используй при реализации или правке доменного слоя в Python-проекте по DDD/гексагональной архитектуре. Триггеры — добавление или изменение агрегата, проекции, объекта-значения, доменного сервиса, фабрики, абстрактного repository-интерфейса в `domain/`; реализация заданных инвариантов, поведения, версионирования, доменных ошибок и состояний. Не применять для определения требований и слоёв `application/`, `infrastructure/`, `presentation/`.4---56# Python DDD Domain Layer Writing78Общие правила оформления Python-кода брать из `$python-code-style-writing` и не9дублировать здесь; этот скил определяет только контракты domain-слоя.1011## Quick Start12131. Выдели из задачи точный состав агрегатов, сущностей, проекций, полей, операций, состояний, инвариантов и отказов. Не дополняй его типовыми элементами.142. Создай для каждого заданного поля отдельный неизменяемый объект-значение. Внутри доменной модели не используй сырые примитивы.153. Создай поддиректорию `domain/<name>/` и последовательно: `value_object.py` → `aggregate.py` (или `projection.py`) → обязательный `factory.py` → только требуемые `repository.py` и `service.py`.164. Разреши создание и восстановление агрегата или проекции только через фабрику. `new()` и `restore()` принимают готовые VO; идентификатор всегда передаёт вызывающий слой.175. Реализуй только заданные состояния, переходы, инварианты, поведение, семантику повторов и проверки версий.186. После всех проверок выполни мутацию агрегата и увеличь версию ровно один раз. При отказе состояние и версия не меняются.197. Проекция принимает заданную версию извне и самостоятельно её не увеличивает. Не добавляй ID-parity без явного требования.208. Различимые доменные бизнес-отказы представь отдельными типами.219. Не создавай коллекцию доменных событий: тип сохранённого изменения, version storage, outbox и публикация принадлежат внешним слоям.2210. Обнови витрину `__init__.py` поддиректории.2311. Внутри `domain/` запрещены импорты из `application/`, `infrastructure/`, `presentation/` и внешних библиотек.2412. Все приватные поля начинаются с `_` и экспонируются без возможности внешней мутации.2513. Реализуй однозначно заданные части; не придумывай недостающую бизнес-семантику для остальных.2627## When to Apply2829### Триггеры активации3031**По расположению файлов** — любая правка/создание в `domain/`:32- `domain/<aggregate>/aggregate.py`, `domain/<aggregate>/factory.py`, `domain/<aggregate>/service.py`, `domain/<aggregate>/repository.py`, `domain/<aggregate>/value_object.py`33- `domain/<projection>/projection.py` и связанные34- Общие файлы: `domain/value_object.py`, `domain/error.py`, `domain/aggregate.py`, `domain/projection.py`3536**По концепциям, упомянутым пользователем:**37- DDD-термины: «агрегат», «aggregate», «value object», «VO», «доменная сущность», «entity», «проекция», «projection», «доменный сервис», «domain service», «фабрика», «factory», «репозиторий-интерфейс».38- Бизнес-инварианты, инкапсуляция бизнес-логики, неизменяемость состояния.39- Оптимистичный параллелизм / версионирование агрегата.40- Управление состоянием сущности (`active`/`deleted`/`frozen`).41- Read-модели, проекции из других bounded context.4243**По типам задач:**44- Добавление нового агрегата или проекции.45- Добавление/изменение VO.46- Добавление/изменение метода поведения агрегата (нового мутатора).47- Добавление доменного сервиса для проверки конкретного бизнес-правила.48- Добавление абстрактного repository-интерфейса в domain.49- Расширение иерархии доменных ошибок.5051### Анти-триггеры5253- Правка `application/` (use cases, commands, queries, DTO, ports).54- Правка `infrastructure/` (psycopg-репозитории, NATS-адаптеры, миграции).55- Правка `presentation/` (FastAPI routers, background workers).56- Импорт-рефакторинг, переименование без изменения структуры.5758### Предусловия5960- Проект следует гексагональной архитектуре с выделенным `domain/`-слоем.61- В `domain/` запрещены импорты из `application/`, `infrastructure/`, `presentation/`. Допустимы только импорты внутри `domain/` и из стандартной библиотеки.62- Состав и семантика доменной модели заданы входными требованиями. Скил выбирает63 способ реализации на Python, но не проектирует новые бизнес-контракты.6465## Package Structure6667```68domain/69├── __init__.py ← пустой, не делаем re-export верхнего уровня70│71├── value_object.py ← общие VO (Version, DomainObjectName, State)72├── error.py ← иерархия доменных ошибок73├── aggregate.py ← базовые AggregateRoot / AggregateRootWithState74├── projection.py ← базовые Projection / ProjectionWithState75│76├── <aggregate_name>/ ← поддиректория на агрегат77│ ├── __init__.py ← re-export публичного API через __all__78│ ├── aggregate.py79│ ├── value_object.py80│ ├── factory.py81│ ├── repository.py ← только для требуемого доменного поиска82│ └── service.py ← только для заданного межобъектного правила83│84└── <projection_name>/ ← поддиректория на проекцию85 ├── __init__.py86 ├── projection.py ← вместо aggregate.py!87 ├── value_object.py88 ├── factory.py89 ├── repository.py ← только для требуемого доменного поиска90 └── service.py ← только для заданного межобъектного правила91```9293### Содержимое верхнеуровневых файлов9495**`domain/value_object.py`** — VO, разделяемые между агрегатами/проекциями. Правило: VO попадает сюда, **только если используется ≥ 2 разными модулями ИЛИ базовыми классами**. Иначе — в `domain/<name>/value_object.py`.9697**`domain/error.py`** — единая публичная иерархия доменных ошибок, включая98специализированные типы различимых бизнес-исходов.99100**`domain/aggregate.py`** — базовые `AggregateRoot` и `AggregateRootWithState`. Других классов в этом файле быть не должно.101102**`domain/projection.py`** — базовые `Projection` и `ProjectionWithState`.103104### Реализация заданного типа105106**Агрегат:**107- Реализуй заданную границу и только перечисленное поведение.108- Изменяй принадлежащие объекты только через корень агрегата.109- После успешной изменяющей операции увеличивай версию ровно один раз.110111**Проекция:**112- Реализуй ровно перечисленные поля и операции локального представления.113- Принимай новую версию извне и не инкрементируй её внутри domain.114- Не принимай transport-модели или application DTO.115- В файле используй `projection.py` и базовый `Projection` / `ProjectionWithState`.116117### Правила импортов внутри `domain/`118119**Разрешено:**120- Импорт из стандартной библиотеки (`uuid`, `decimal`, `datetime`, `enum`, `dataclasses`, `typing`, `abc`).121- Импорт между модулями `domain/` (например, `from domain.tenant import TenantID` в `personal_transaction/aggregate.py`).122- Импорт из общих файлов (`domain.value_object`, `domain.error`, `domain.aggregate`, `domain.projection`).123124**Запрещено:**125- Импорты из `application/`, `infrastructure/`, `presentation/`.126- Импорты внешних библиотек (никаких `pydantic`, `psycopg`, `fastapi`).127- Циклические импорты между поддиректориями. При риске цикла — выносим общий VO в `domain/value_object.py`.128129### Именование130131- Модуль агрегата/проекции — **snake_case в единственном числе** (`personal_transaction`, не `personal_transactions`).132- Класс агрегата — **PascalCase, существительное в единственном числе** (`PersonalTransaction`, `Tenant`).133- ID-VO — **`<Aggregate>ID`** (`PersonalTransactionID`, не `PersonalTransactionId`).134- Фабрика — **`<Aggregate>Factory`**.135- Repository-интерфейс — **`<Aggregate>ReadRepository`**.136- Сервис именуется по конкретному бизнес-правилу: `UserUniquenessService`, `TransactionOwnershipPolicy`.137138## Error Hierarchy139140### Полная иерархия141142```143DomainError (msg, subject, data)144├── ValueObjectError145│ └── ValueObjectInvalidDataError146└── EntityError147 ├── EntityInvalidDataError148 ├── EntityVersionError149 ├── EntityIdempotentError150 ├── EntityPolicyError151 ├── EntityAlreadyExistsError152 └── EntityNotFoundError153```154155Вся публичная иерархия живёт в `domain/error.py`. Для каждого бизнес-исхода,156который вызывающий слой должен отличать от остальных, создавай отдельный тип,157наследуя его от подходящей общей категории. Не создавай разные типы для внутренних158проверок с одинаковым наблюдаемым смыслом.159160### Базовый класс161162```python163from typing import Any164165166class DomainError(Exception):167 def __init__(168 self,169 msg: str,170 subject: str,171 data: dict[str, Any] | None = None,172 ) -> None:173 super().__init__(msg)174 self.msg = msg175 self.subject = subject176 self.data = data or {}177178 def __repr__(self) -> str:179 return (180 f"{self.__class__.__name__}("181 f"msg={self.msg!r}, subject={self.subject!r}, data={self.data!r})"182 )183184185class ValueObjectError(DomainError):186 pass187188189class ValueObjectInvalidDataError(ValueObjectError):190 pass191192193class EntityError(DomainError):194 pass195196197class EntityInvalidDataError(EntityError):198 pass199200201class EntityVersionError(EntityError):202 pass203204205class EntityIdempotentError(EntityError):206 pass207208209class EntityPolicyError(EntityError):210 pass211212213class EntityAlreadyExistsError(EntityError):214 pass215216217class EntityNotFoundError(EntityError):218 pass219```220221### Поля222223- **`msg`** — сообщение об ошибке на языке домена. Пример: `"новое состояние идентично текущему"`, `"арендатор удален"`, `"только владелец может работать с категорией"`.224- **`subject`** — человекочитаемая метка предмета ошибки на языке домена. Это **не** имя класса/модуля, а название агрегата/VO/проекции в терминах бизнеса. Для агрегатов и проекций берётся из общего `domain_object_name.name`, для остальных VO задаётся явно. Примеры: `"арендатор"`, `"категория транзакций"`, `"проекция пользователя"`, `"название категории"`, `"версия агрегата"`.225- **`data`** — безопасный структурированный контекст ошибки для вызывающего слоя.226 Опционально, по умолчанию `{}`. Domain не логирует ошибки и не определяет их227 transport-представление.228229### Конвенция формирования `data`230231**Ключи верхнего уровня — английские, в snake_case, отражают тип сущности:**232233| Тип ошибки | Ключ верхнего уровня | Значение |234|---|---|---|235| Ошибка одного агрегата | имя сущности в ед. ч.: `"tenant"`, `"transaction"`, `"category"`, `"user"` | `dict` с полями этой сущности |236| Ошибка с участием нескольких сущностей одного типа | имя во мн. ч.: `"categories"`, `"transactions"` | `list[dict]` |237| Ошибка VO | имя поля VO: `"version"`, `"name"`, `"transaction_type"` | примитив или `dict` |238239**Внутри блока сущности — поля в snake_case (английские), значения — примитивы для JSON:**240241```python242# Ошибка одного агрегата243{"tenant": {"tenant_id": UUID(...), "state": "active"}}244245# Ошибка с двумя агрегатами разных типов246{247 "tenant": {"tenant_id": UUID(...)},248 "transaction": {"owner_id": UUID(...)},249}250251# Ошибка с коллекцией252{253 "transaction": {"transaction_id": UUID(...)},254 "categories": [255 {"category_id": UUID(...), "name": "еда"},256 {"category_id": UUID(...), "name": "транспорт"},257 ],258}259260# Ошибка VO261{"version": 0}262{"transaction_type": "wrong_value"}263{"money_amount": {"amount": "-10.50", "currency": "ruble"}}264```265266**Правила значений:**267- `UUID` оставляем как `UUID`-объект, не строкой — сериализатор API сам приведёт.268- `Decimal` приводим к `str` (`str(amount)`), чтобы избежать потери точности при JSON-сериализации.269- `datetime` оставляем как есть, сериализатор API приведёт.270- `Enum` берём `.value` (строку), не сам enum.271272### Семантика подклассов273274| Класс | Когда бросать |275|---|---|276| `ValueObjectInvalidDataError` | VO получил невалидные данные при конструировании. В `__post_init__` или `from_str`. |277| `EntityInvalidDataError` | Операция над сущностью невозможна из-за её состояния или входных данных, не подпадающих под идемпотентность/политику. Типичные случаи: попытка изменить удалённую сущность (через `_check_state`), пустые данные на входе, несовместимые входные данные. |278| `EntityVersionError` | Нарушение контракта версии сущности или проекции, включая получение устаревшей версии. |279| `EntityIdempotentError` | Повторный вызов должен завершиться доменным отказом согласно заданной семантике. Не используй его автоматически для каждого равенства значений. |280| `EntityPolicyError` | Нарушение бизнес-политики, не связанное с состоянием самой сущности (только владелец может, только админ может, доступ запрещён из-за состояния субъекта). |281| `EntityAlreadyExistsError` | Бизнес-правило уникальности обнаружило уже существующую сущность. |282| `EntityNotFoundError` | Бизнес-правило требует существования связанной сущности, но она не найдена. |283284### Правила вызова285286- **`data` пропускаем, если её нет** — не передавать `data={}` явно.287- **В агрегатах используем `_error_data` хелпер** — он подставляет `subject` из `domain_object_name.name` и добавляет ID агрегата в `data`: `raise EntityIdempotentError(**self._error_data(msg="...", data={...}))`.288- **В VO и в сервисах** заполняем поля явно (хелпера нет).289290## Public API of a Module (`__init__.py` and `__all__`)291292### Что попадает в витрину293294| Категория | Примеры |295|---|---|296| Класс агрегата / проекции | `Tenant`, `User` |297| Фабрика | `TenantFactory`, `UserFactory` |298| Все VO агрегата/проекции | `TenantID`, `TenantState`, `TenantStatus` |299| Repository-интерфейс (если есть) | `TenantReadRepository` |300| Domain-сервисы (если есть) | `UserUniquenessService`, `TransactionOwnershipPolicy` |301302**Не попадает:**303- Приватные хелперы (имя с `_`).304- Базовые классы из общих файлов (`AggregateRoot`, `Projection`, `DomainError`) — импортируются напрямую.305306### Шаблон `__init__.py`307308```python309from domain.<aggregate>.entity import <Aggregate>310from domain.<aggregate>.factory import <Aggregate>Factory311from domain.<aggregate>.repository import <Aggregate>ReadRepository312from domain.<aggregate>.service import <Aggregate>UniquenessService313from domain.<aggregate>.value_object import (314 <Aggregate>ID,315 <Aggregate>Name,316 <Aggregate>State,317)318319__all__ = [320 "<Aggregate>",321 "<Aggregate>Factory",322 "<Aggregate>ID",323 "<Aggregate>Name",324 "<Aggregate>ReadRepository",325 "<Aggregate>State",326 "<Aggregate>UniquenessService",327]328```329330**`__all__` строго в алфавитном порядке.**331332### Top-level `domain/__init__.py` — пустой333334Не делаем re-export всех агрегатов на верхний уровень. Импорты на стороне идут через поддиректории.335336### Правила импорта337338| Где | Откуда импортируем |339|---|---|340| Внутри `domain/<aggregate>/*.py` (тот же модуль) | Прямые пути: `from domain.<aggregate>.entity import ...` |341| Из `domain/<other_aggregate>/*.py` | Витрина: `from domain.<other_aggregate> import ...` |342| Из `application/`, `infrastructure/`, `presentation/` | Витрина: `from domain.<aggregate> import ...` |343| Общие файлы (`domain/aggregate.py`, `domain/error.py`, ...) | Прямые пути: `from domain.error import ...` |344345## References346347- **`references/value_objects.md`** — три типа VO (Identity, Validated, Enum) + Multi-field, правила иммутабельности, нормализация, валидация.348- **`references/aggregates.md`** — базовые классы `AggregateRoot` / `AggregateRootWithState`, паттерны агрегатов, фабрики агрегатов, особый случай расширенного состояния.349- **`references/projections.md`** — базовые классы `Projection` /350 `ProjectionWithState`, операции, фабрики и правила входящей версии.351- **`references/services_and_repositories.md`** — сервисы конкретных бизнес-правил и необходимые им repository-интерфейсы.352- **`references/testing.md`** — unit-тесты domain-инвариантов, версии, фабрик, fixtures и покрытие.353- **`references/checklists.md`** — пошаговые чек-листы добавления агрегата и проекции.