Перейти к основному содержимому

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:

  1. handler валидирует request и загружает aggregate;
  2. handler собирает внешний context в typed snapshots/ports;
  3. aggregate или domain policy принимает решение;
  4. aggregate меняет состояние и поднимает domain events;
  5. 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 ответь на пять вопросов:

  1. Что здесь является source of truth?
  2. Какой aggregate отвечает за корректность решения?
  3. Какие инварианты нельзя нарушать даже при race conditions?
  4. Что должно произойти после успешного решения?
  5. Что будет, если операция придет повторно или в неправильном порядке?

Если ответы завязаны на доменную модель и state transitions, нужен rich model.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы