Структура кода в папке Domain по DDD
- суббота, 13 декабря 2025 г. в 00:00:09
Последние 5 лет я изучаю и практикую DDD как стратегический, так и тактический, везде, где представляется возможным. И вот чем больше я погружался в тактическую часть - тем чаще возникал вопрос "это я дурак или лыжи не едут". Пришло понимание того, что огромнейшая часть сообщества структурирует код своего контекста забыв о самом главном:
Структура и язык кода должны соответствовать бизнес-домену

В данной главе огромная возможность написать много текста, почему следующие два примера плохи с точки зрения OOD, Cohesion/Coupling, расширяемость, сопровождение, сложность изучения кодовой базы и тд - но я этим заниматься не буду. Об этом есть много информации в сети.
Вариант 1
Domain/
Entity/
ValueObject/
Repository/
DomainService/
Event/Вариант 2
Domain/
Model/
Services/
Repository/Глядя на эту структуру - первое что приходит в голову - хм... наверное мы все же позабыли пункт про соответствие бизнес-домену и запомнили только про 4 раздел нашей любимой синей книги. А может мы вовсе прочитали только статью/книгу "DDD на языке Х" и даже не имеем мысли о том что здесь что-то не так.
В своем домене мы сделали центром внимания патерны. Технические детали из которых состоит бизнес логика нашего контекста.
Здесь нет отображения бизнес-домена. Придерживаясь "кричащей архитектуры", которая как одна из многих идей легла в основу DDD - открывая папку домена - мы начинаем изучать знания о бизнесе. Но увы нет. Мы начинаем изучать патерны благодаря которым мы отобразили те самые бизнес знания.
Не забывайте о том что если вы используете DDD - то скорее всего у вас сложная и большая предметная область. А это значит, что чем больше вы изучаете предметную область(реализуете проект в контексте тактической части) - тем больше под-папок будет появляться внутри ваших основных папок. Здравствуйте вы приготовили спагетти.
Первое правило Бойцовского клуба: никому не рассказывать о Бойцовском клубе.
Не кричать о патернах. Да, мы знаем патерны и думаем о границах агрегата, связях между ними. Выделением VO с инвариантами. Пишем доменные сервисы и DTO для входных параметров и для результата работы сервиса. Наши агрегаты генерируют события а доменные сервисы могут завершиться с ошибкой. Но мы не делаем акцент на патернах. Мы делаем акцент на предметной области и её особенностях:
Domain/
Template/
Attribute\
Attribute.php
AttributeType.php
AttributeValue.php
AttributeStyle.php
Template.php
TemplateChanged.php
TemplateArchivated.php
TemplateRepositoryInterface.php
CalculateRecomendationScore/
RecomendationScore.php
CalculateRecomendationScore.php
ScoreUncalculatable.php
ProductCode.php
ProductPrice.php
Product.php
ProductRepositoryInterface.php
AssignProductToTemplate/
MoveProductToTemplate.php
ProductTemplateChanged.php
ProductCannotBeAssignedToTemplate.phpЗдесь я считаю всё по канону:
Мы видим из чего состоит предметная область нашего контекста. Мы в принципе видим предметную область :D а не группы типов из которых она состоит.
Мы видим что речь о продукте. Мы видим что у продукта есть support-sub-domain Шаблон. Нам не интересно ProductPrice это VO или Entity, нам важно какую бизнес задачу решает этот объект. Мы видим бизнес-фичу "Назначить продукт на шаблон" и все её внутренние состовляющие которые она в себе содержит (событие и ошибку, чаще всего на практике там будет DTO для input/output параметров сервиса).
Новый разработчик пришел в команду - ему не нужна документация которую ни разу не обновляли после написания. Ему всего лишь нужно открыть репозиторий с кодом и начать погружать в предметную область проекта в который он пришел.
Свод правил по которым будет строится структура - неописуем. От проекта к проекту он будет разный основываясь на особенностях единого языка и структуры предметной области которую мы реализуем. Для себя я вывел парочку на основе лингвистики и и "здравого смысла".
Фичи (aka доменный сервис) именуем в повелительном падеже: Сделай что-то. Создай товар. Обнови цену. Проверь имя на уникальность. Посчитай очки популярности.
Саб-домены именуем существительными
Не создавайте VO просто так
Если ваше значение не содержит бизнес инвариантов - не стоит создавать ради этого класс, стандартных типов вашего языка может вполне хватить.
Делайте VO общими и выносите их Shared для типичных вещей. Например PositiveInteger, NotEmptyString (на любителя)
Доменные события именуем в прошедшем времени
Ошибки именуем с действием в отрицательной форме
Вообще ошибка - это результат юз кейса. Если мы этого придерживаемся и не строим свою логику на ошибках - классов с ошибками будет немного. Достаточно будет одного Shared\DomainError.
Базовый пример это repo->find(): null|Object + repo->get() :Object.
Не стоит оборачивать get в try catch если вам нужно что-то сделать когда агрегата не существует. Это логика, а для логики есть if. В большинстве случаев работать с ошибками мы будем только на инфраструктурном уровне.
Не бойтесь рефакторинга - это бесконечный процесс если вы не потратили 100500 часов на сеансы EventStorming со своими бизнес экспретами
С течением времени вы будете узнавать свою предметную область все больше и больше. К вам будет приходить понимание того, что все таки этот кусок кода нужно вынести из Foo в Bar и окажется что этот доменный сервис является частью нового саб-домена.
На своей практике я применил данную структуру в двух больших продуктах: один был монолитным, второй MSA. Если выделены контексты - не важно в какой кодовой базе лежит соседний контекст. Единственный нюанс - в монолите сложно сохранять "чистоту" зависимостей контекста. Но книга Влада Хонова про кауплинг открывает новый взгляд на это.
На этом и все. Написать данную статью как крик души я собирался несколько лет. Со многими практикующими DDD разработчиками я общался и лишь немногие пришли к тем же самым выводам, что пришел и я. Но радует тот факт, что на моем пути все же встречались люди в сообществе которые вывели для себя +- такое же видение как должна быть реализована тактическая часть DDD.