DDD И Rich Model: Правила И Практика
Назначение
Этот документ является главным нормативным файлом по применению DDD, rich model, aggregates, value objects, domain services и domain events в Pin. Архитектурные решения, backend rules и практические guides должны ссылаться на него вместо повторения одних и тех же положений.
Это code-time руководство для разработчиков backend, а не содержание ТЗ. Термины реализации (handler, validator, aggregate root, domain service, CQRS-поток) относятся к коду. Спецификации-постановки (
docs/06-services/**) на этот словарь не ссылаются и его не цитируют — они описывают поведение на аналитическом уровне.
Когда Выбирать Rich Model
Rich model обязателен, когда одновременно присутствует хотя бы часть следующих признаков:
- у объекта есть lifecycle и допустимые/недопустимые переходы;
- объект защищает инварианты, нарушение которых критично для бизнеса;
- решение зависит от комбинации нескольких полей и текущего состояния;
- изменение состояния порождает audit-sensitive или financially-sensitive последствия;
- есть конкурирующие изменения и требуется защита от invalid transitions;
- downstream side effects должны происходить только после успешного domain decision.
Типичные категории:
- ledger/accounting;
- документы согласования;
- заявки и approval flows;
- заказы, движения остатков и резервирования;
- compliance и safety decisions;
- сложные incentive, pricing или settlement rules.
Когда Допустим Anemic Model
Anemic model допустим, если:
- объект является reference data или admin-managed dictionary;
- поведение ограничивается CRUD и простыми validation rules;
- нет самостоятельных инвариантов beyond schema validation;
- read model существует только как projection, search document, dashboard row или export row.
Решение "анемичная или rich" должно приниматься осознанно, а не по умолчанию.
Aggregate Root
Обязательные Правила
- Aggregate Root является единственной точкой изменения внутреннего состояния aggregate.
- Изменение child entities напрямую из handler-а или ORM mapping-конфигурации запрещено.
- Aggregate защищает invariants кодом, а не только DB constraints.
- Один command handler обычно изменяет один aggregate root в одной локальной транзакции.
- Aggregate не знает про
DbContext,HttpContext,ILogger,IServiceProvider, message bus и внешние API. - Aggregate не тянет чужие aggregates как object references; только foreign ids или snapshots.
Практические Рекомендации
- Делай public API aggregate небольшим и глагольным:
Approve,Reject,Reserve,Assign,Close. - Не раскрывай mutable collections наружу.
- Не допускай public setters для полей, участвующих в инвариантах.
- Инициализацию сложных aggregates делай через factory methods, если конструктор теряет читаемость.
- Не превращай aggregate в god object; если invariants не связаны общей consistency boundary, вероятно это два aggregates.
Инварианты
- Инварианты делятся на structural, lifecycle, quantitative, authorization-sensitive и temporal.
- Structural invariant: форма и допустимость данных внутри aggregate.
- Lifecycle invariant: допустимые переходы между статусами.
- Quantitative invariant: суммы, лимиты, counters, остатки, reserved amounts.
- Authorization-sensitive invariant: действие допустимо только при наличии предварительно проверенного domain context.
- Temporal invariant: cooling-off, expiration, deadline, lock window.
Для каждого важного aggregate нужно уметь ответить:
- что всегда должно быть истинно;
- какие переходы запрещены;
- что считается частичным/недопустимым состоянием;
- как выглядит компенсация ошибки.
Value Objects
Value objects обязательны, если они:
- заменяют primitive obsession;
- инкапсулируют validation rules;
- делают типовую ошибку невозможной на уровне компиляции;
- являются частью ubiquitous language домена.
Рекомендации
- Value object должен быть immutable.
- Equality строится по значению, а не по identity.
- Нормализация и validation выполняются при создании.
- Строковые и числовые значения без domain wrapper не должны течь через core-domain без причины.
Domain Methods
Что Должны Делать
- проверять invariants и preconditions;
- изменять состояние aggregate;
- порождать domain events при успешном значимом изменении;
- возвращать typed result для controlled business failures.
Что Не Должны Делать
- ходить в сеть или БД;
- читать ambient context;
- принимать неструктурированные DTO;
- содержать инфраструктурные retry/logging concerns;
- silently ignore invalid transitions.
Exceptions vs Result
- Controlled business failures возвращаются через typed result/error.
- Исключения используются для truly exceptional cases: broken invariant due to bug, impossible state, corrupted data, programmer error.
- Domain exception допустим, если состояние невозможно корректно представить через result и это реально ошибка реализации, а не обычный business rejection.
Domain Services
Domain service нужен, когда правило:
- не помещается естественно в один aggregate;
- работает с несколькими domain objects одного bounded context;
- остается pure domain logic без I/O.
Нельзя превращать domain service в скрытый application service. Если внутри есть repository, HTTP client, cache или bus, это уже не domain service.
Policies И Specifications
- Reusable business rules выносятся в named policies.
- Policies должны говорить на domain language, а не на техническом жаргоне.
- Specifications применяются для reusable predicates, но не должны скрывать критичные решения так глубоко, что теряется читаемость use case.
- Не создавай specification abstraction только ради абстракции. Она должна реально уменьшать дублирование или формализовать сложное правило.
Domain Events
- Domain event фиксирует факт уже принятого domain decision.
- Domain events являются внутренним артефактом bounded context.
- Публикация integration events происходит через documented mapping и outbox.
- Domain event payload должен быть bounded, typed и без чувствительных избыточных данных.
- Не порождай event "на всякий случай"; событие должно соответствовать реально значимому изменению модели.
Rich Model И CQRS
Рекомендуемый поток для command:
- handler валидирует request и загружает aggregate;
- handler собирает внешний context в typed snapshots/ports;
- aggregate или domain policy принимает решение;
- aggregate меняет состояние и поднимает domain events;
- handler сохраняет aggregate, audit и outbox в одной транзакции.
Нельзя:
- размещать core business decision целиком в handler-е, если оно зависит от состояния aggregate;
- дублировать одну и ту же rule одновременно в validator, handler и aggregate без явного разделения ответственности;
- делать query side источником истины для command-side decisions высокого риска.
Rich Model И Persistence
- ORM mapping подчиняется модели, а не наоборот.
- Private setters и backing fields допустимы, если они защищают инварианты.
- Lazy-loading для aggregates нежелателен, если он размывает границу изменения состояния.
- DB constraints усиливают инварианты, но не заменяют domain checks.
- Snapshot/replica/JSONB documents не должны размывать ownership aggregate.
Rich Model И Тестирование
Минимум для rich aggregate:
- success path;
- illegal transitions;
- boundary values;
- repeated call behavior;
- domain event emission;
- invariants after mutation.
Для domain services:
- pure unit tests без инфраструктуры;
- таблицы сценариев для правил и комбинаций входных данных;
- отдельные tests для temporal and quantitative logic.
Антипаттерны
- Anemic domain с public setters и "умным" handler-ом на 300 строк.
- Aggregate, который знает о пяти внешних сервисах.
- Domain methods, принимающие controller DTO.
- Validation, размазанная хаотично между UI, validator и aggregate без разделения ответственности.
- Псевдо-DDD, где у сущности есть методы
SetStatusиSetAmount, но нет инвариантов и решений. - Aggregate, который агрегирует слишком много несвязанных обязанностей.
Практическое Правило Выбора
Перед реализацией use case ответь на пять вопросов:
- Что здесь является source of truth?
- Какой aggregate отвечает за корректность решения?
- Какие инварианты нельзя нарушать даже при race conditions?
- Что должно произойти после успешного решения?
- Что будет, если операция придет повторно или в неправильном порядке?
Если ответы завязаны на доменную модель и state transitions, нужен rich model.
Обратные ссылки
Автоссылки: где используется этот документ.
- adr (1): ADR-0039: DDD Rich Model vs Anemic Model — Правила Выбора
- 01-ddd-domain-model.md (1): DDD и Domain Model: Практическое Руководство
- backend-rules.md (1): Правила Backend Разработки