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

Давайте посмотрим на базовую структуру папки Domain
В данной главе огромная возможность написать много текста, почему следующие два примера плохи с точки зрения OOD, Cohesion/Coupling, расширяемость, сопровождение, сложность изучения кодовой базы и тд - но я этим заниматься не буду. Об этом есть много информации в сети.
Вариант 1
Domain/
Entity/
ValueObject/
Repository/
DomainService/
Event/
Вариант 2
Domain/
Model/
Services/
Repository/
Глядя на эту структуру - первое что приходит в голову - хм... наверное мы все же позабыли пункт про соответствие бизнес-домену и запомнили только про 4 раздел нашей любимой синей книги. А может мы вовсе прочитали только статью/книгу "DDD на языке Х" и даже не имеем мысли о том что здесь что-то не так.
В своем домене мы сделали центром внимания патерны. Технические детали из которых состоит бизнес логика нашего контекста.
Здесь нет отображения бизнес-домена. Придерживаясь "кричащей архитектуры", которая как одна из многих идей легла в основу 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/
AssignProductToTemplate.php
ProductTemplateAssigned.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.
UPD Пример от Вона Вернона (Автора красной книги)
Комментарии (35)

g6uru
12.12.2025 09:47Чтобы в доме был порядок, "вещи должно быть удобно класть", это важнее, чем правило "вещи должно быть удобно брать".
При разработке поддерживаемой архитектуры, которую пишут большем чем 1 человек и дольше чем одну неделю, удобно пользоваться тем же правилами.
Еще вопрос в однозначности интерпретации.
Порядок + однозначность интерпретации > выразительность модели через файловую структуру.

Dr10s Автор
12.12.2025 09:47Что может быть более однозначно чем сама модель, а не хаотичный набор Патерсон где что бы понять фичу нужно просмотреть все паттерны и найти среди них то что относится к фиче

g6uru
12.12.2025 09:47Модель сама по себе не снимает неоднозначность.
Один и тот же кусок логики можно интерпретировать по-разному.Структура проекта - это не про паттерны и не про выразительность, а про однозначную навигацию: где искать use-case, где доменные правила, где ошибки и события.
Если для каждой фичи это место угадывается одинаково - интерпретация становится однозначной. Если нет - приходится "восстанавливать модель по коду", а это дорого.

Dr10s Автор
12.12.2025 09:47Согласен. В моём мире именно структурирование по типам заставляет "восстанавливать модель по коду"

Tepex
12.12.2025 09:47Это как если бы директор кладбища хранил бы руки в одном месте, ноги - в другом. Потому что ему так удобно для логистики, эффективного землепользования и т.д. А вот родственникам совсем не удобно.

abyrvalg
12.12.2025 09:47Неверная аналогия.
Представьте лучше, что вы продаёте пирожки. Поставщик привёз вам новую партию, но свалил в одну кучу пирожки с мясом, капустой и яйцом. Тут приходит клиент и требует пирожок с капустой. Какие будут ваши действия?

Tepex
12.12.2025 09:47Если это комплексный обед, состоящий из нескольких пирожков разных типов, то да -- их нужно организовывать в такие кучки. Вопрос ведь не в том, чтобы свалить все в одну кучу, а в принципе структуризации -- по контексту или по типу. И если в домене выделяются несколько контекстов, то следует организовывать структуру именно по этому аспекту.

abyrvalg
12.12.2025 09:47в принципе структуризации -- по контексту или по типу
А, в этом плане. Тогда полностью солидарен.

AlexViolin
12.12.2025 09:47Куда поместить доменный сервис, который работает сразу с двумя агрегатами?

Dr10s Автор
12.12.2025 09:47Зависит от сути сервиса.
Например CalculateRecomendationScore.php может работать и с шаблоном и с продуктом(смотреть остатки продуктов которые входят в шаблон например)
Так же и сервис MoveProductToTemplate.php может изменять продукт и так же работать с шаблоном например для проверки подходит ли этот продукт под нахождение в этом шаблоне.
Каждый случай уникален, особенно в случаях когда бизнес не позволяет сделать нам eventual consistency для обновления двух агрегатов через события.

AlexViolin
12.12.2025 09:47То есть предполагается, что доменный сервис всегда находится в папке определённого агрегата даже когда работает с несколькими агрегатами?

ivvi
12.12.2025 09:47Не является ли проблемой с точки зрения Единого Языка, что у вас в каталоге AssignProductToTemplate/ используется три разных глагола (Assign, Move, Changed)?
Как будто бы вложенные файлы должны быть AssignProductToTemplate.php и ProductTemplateAssigned.php

olku
12.12.2025 09:47PositiveInteger является бизнес инвариантом или валидатором?

Dr10s Автор
12.12.2025 09:47И тем и тем, например для VO скидка можно сделать VO и указать там правило что скидка не может быть 0 и меньше 0, теже правила скорее всего будут у цены, цена продукта не может быть 0 и ниже 0. Такие вещи объединяются в shared VO что бы не плодить слишком много однотипных VO ради повторяющихся инвариантов

olku
12.12.2025 09:47Вряд ли валидатор это реализация VO, для них фабрики предусмотрены. Нет ничего страшного в экшепшенах в них, даже для PHP

ljadrbln
12.12.2025 09:47Спасибо за статью. Вопрос из практики.
Часто сталкиваюсь с ситуацией, когда Domain начинает разрастаться, и граница между доменной логикой и прикладными сервисами становится размытой.
Например: есть доменная сущность
Orderи правило пересчёта итоговой суммы с учётом скидок, налогов и внешних ограничений (лимиты, акции).
Формально это бизнес-правило, но часть данных и решений приходит из Application / Infrastructure.В таких случаях вы оставляете логику в Domain (через абстракции/политики) или предпочитаете выносить оркестрацию в Application?
Есть ли у вас практический критерий, по которому вы принимаете это решение?
Dr10s Автор
12.12.2025 09:47Когда стоит вопрос "положить в прикладной или доменный слой" - я прибегаю абстрагированию от ИТ. Переношу сценарий на 100500 лет назад где не было компьютеров а люди жили в пещерах. И все те вещи которые есть как и в пещерное время так и сегодня независимо от технологий - 100% кладу в domain.
Но такое тоже не всегда можно провернуть. Если у нас есть несколько разных use case что бы создать продукт, то скорее всего различия между этими use case будут аркестрироваться в прикладном слое.
Например если продукт создаёт главный админ мы его просто создаём.
А если продукт создаёт саппорт-менеджер - нам нужно проверить несколько внешний условий и при их успехе создать продукт - саму логику условий будет в домене а ее использование в прикладном слое.
В моем варианте будет папка Domain/Create product/ и в ней будут сервисы/интерфейсы/политики отвечающие за создание продукта которые будут вызываться в разных use case в разной вариации.

ljadrbln
12.12.2025 09:47Спасибо, хороший пример с разными ролями и сценариями.
Тогда попробую зафиксировать границу, чтобы проверить, правильно ли я вас понял.
Я для себя обычно формулирую так:
Domain отвечает за то, что допустимо в предметной области (инварианты, политики, правила),
Application - за то, когда, кем и в каком порядке эти правила применяются.Поэтому для меня тревожный сигнал, если Domain-сервис начинает:
знать о ролях пользователей,
различать use case'ы,
принимать решения о последовательности шагов.
На мой взгляд, в этот момент он уже превращается в 'скрытый use case', просто лежащий не в том слое.
Вопрос: есть ли у вас практические стоп-сигналы, по которым вы понимаете, что доменный сервис пора упрощать или дробить, чтобы он не начал оркестрировать сценарии?

Dr10s Автор
12.12.2025 09:47Здесь без конкретики сложно что-то придумать. Мне лично очень помогают атрефакты Event Storming. По ним становится понятнее - это часть домена или часть юзкейса.
Но на практке Event Storming применятся очень редко из-за его дороговизны - все нюансы становятся видно только со временем разработки. Чем больше мы узнаем/реализуем нашу модель - тем больше мы начинаем понимать, что этот сервис стоит вынести в юзкейс. А этот юзкейс вообще не юзкейс, а бизнес фича которая не зависит от входных условий.
Возможно, более базово погрузится в этот вопрос может помочь книга по UML от Ивара Якобсона, а точнее её раздел про варианты использования. По слухам бородатых дядек - это можно назвать первоисточником определения use case.

gun_dose
12.12.2025 09:47А что думаете о подходе, когда папки организованы, как в первом случае, но все интерфейсы лежат в корне домена? То есть по набору интерфейсов сразу видно, что делает домен, а реализация уже по паттернам

Dr10s Автор
12.12.2025 09:47Данный подход очень удобен и полезен для маленьких проктов/bounded context. Где нет много сущностей, сложной логики, больших связей между сущностями и реакций на действия сущностей. Т.к. основная проблема данного подхода - чем больше проект - тем сложнее в нем ориентироваться. в папке Entity появляются подпапки в подпапках еще подпапки и мы получаем набор классов которые непонятно как живут друг с другом.
Всякие поддерживающие контексты которые не требуют дорогой проработки и постоянного развития и сопровождения - очень выгодно реализовывать таким подходом. Мы быстро накидали код, задеплоили и забыли про него. Он просто выполняет свою функцию и не развивается.
Не знаю хорошо это или плохо но уже произошла какая-то "проф деформация" и уже даже мелкие поддерживающие проекты реализую с помощью packet by feature в domain

Antharas
12.12.2025 09:47ну давайте теперь будем вообще делать один класс для всех сущностей и репозиториев, че мелочится то

RuslanTaghiyev
12.12.2025 09:47Спасибо за статью, интересно увидеть альтернативный взгляд.
Но не смотря на то, что структура по идее соответствуем идеям DDD и в целом аналогична как минимум примерам Вон Вернона, я считаю что в реальности от неё может быть больше проблем чем пользы. Поэтому постараюсь конструктивно расписать в чём я вижу минусы:
Основной минус - сложность для понимания
Вы утверждаете что с такой структурой проще понимать доменную логику:
Мы видим из чего состоит предметная область нашего контекста. Мы в принципе видим предметную область :D а не группы типов из которых она состоит.
Я бы сказал что наоборот. Когда вы анализируете код проекта(и не важно потому что вы разработчик который только пришёл на него, или например архитектор который собирается его декомпозировать), то вам не настолько принципиально какие там доменные ивенты или ВО. Самое главное это понять какие есть агрегейт руты и энтити и дальше уже копать вглубь, чтобы понять как модуль/сабдомен взаимодействует с остальными или какие фичи он реализует и тд. И я не согласен с утверждением что отталкиваться нужно от фич:
Нам не интересно ProductPrice это VO или Entity, нам важно какую бизнес задачу решает этот объект. Мы видим бизнес-фичу "Назначить продукт на шаблон" и все её внутренние состовляющие которые она в себе содержит
Хоть это и звучит логично и бизнес-ориентированно, на практике фич могут быть десятки и сотни, а вот модель – одна. Поэтому важно сначала понять модель, а потом уже переходить к фичам. Если же ваш пакет состоит из сотен классов, то вы убьёте уйму времени просматривая классы которые вам даже не нужны для начала.
В том же EventStorming-е есть типы "ивент" и "агрегат" которые разделены по формам и цветам, чтобы это было нагляднее, поэтому по хорошему даже бизнес должен понимать что это разные типы. Да, можно сказать что в итоге они всё равно находятся все на одной доске чтобы описать бизнес процесс, но в отличие от доски со стикерами в пакете кода вы так сразу не разберётесь что есть что и как они взаимодействуют.
Минус второй - сложность анализа
Помимо этого, намного проще визуально анализировать сложность доменного слоя когда он разделён на чёткие слои - например если у нас 100 сущностей и 5 VO, скорее всего мы делаем что-то не так. Суда же относятся и кол-во доменных сервисов. Вы в статье утвержаете что доменный сервис и фича это одно и тоже:
Фичи (aka доменный сервис) именуем в повелительном падеже: Сделай что-то. Создай товар. Обнови цену. Проверь имя на уникальность. Посчитай очки популярности.
Но это неверно – я даже полез к разным DDD-авторам перепроверить. Все утверждают что доменный сервис это скорее исключение из правил нужное для того чтобы работать с несколькими агрегатами, и которым не нужно злоупотреблять.
Вот например из книги Эванса:
“When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as a standalone interface declared as a SERVICE. Define the interface in terms of the language of the model and make sure the operation name is part of the UBIQUITOUS LANGUAGE. Make the SERVICE stateless.”
...
"SERVICES should be used judiciously and not allowed to strip the ENTITIES and VALUE OBJECTS of all their behavior."
Поэтому для меня кол-во доменных сервисов это зачастую показатель либо того насколько связаны агрегейт руты, либо что модель выраждается и логика начинает писаться не в сущностях и VO. (У нас в DDD-проектах их например почти нет, и каждый раз когда они собираются появиться мы стараемся анализировать нужны ли они вообще).
Можно конечно решать эти проблемы соответствующими аннотациями(т.е. помечать например @ValueObject или @AggregateRoot) или интерфейсами + fitness functions, но имхо это сложнее реализовать, менее наглядно, не всем нужно и не позволяет проводить визуальный анализ.
Алтернативный пример
Вот пример структуры которую использую я в монолитах:
/domain /product /entity /service /value_object /event /another module/subdomainЕсли же у вас микросервисы и действие уже происходит в условном
product-service, то можно обойтись без вложенных пакетов, что будет практически тем же самым что и в ваших примерах в начале.PS: Репозитории я вообще не включаю в доменный слой, тк считаю что даже доменные сервисы не должны иметь зависимостей(хоть DDD это и не запрещает), чтобы не смешивать бизнес и инфра-логику.
Ну и напоследок
В целом я считаю, что оба подхода имеют место быть, хоть и вижу в вашем/классическом больше минусов, по причинам описанным выше. При этом намного важнее я считаю:
Заставить разработчиков вникать в бизнес. Если они это делают то они и с анемичной моделью будут фокусироваться на бизнесе и использовать стратегические паттерны DDD. А если нет, то никакие тактические паттерны DDD вам не помогут, скорее наоборот.
Следить за чистотой кода и доменной области. Не позволять разным агрегатам напрямую ссылаться друг на друга, только по id. Не мешать доменную и инфра-логику. Не создавать изменяемые VO. Сделать рефакторинг постоянным процессом и тд.
Иметь единую структуру пакетов и тактических паттернов в рамках проекта и организации. Если вы планируете проекты которые будут расти и поддерживаться годами то стандартизация имеет огромное значение. Куда важнее чтобы структура пакетов была одинаковой в разных модулях/сервисах, чем то, чтобы она была "правильнее", но только в некоторых. И чтобы у всех было понимание какие тактические паттерны используются на проекте – чтобы не было ситуаций когда кто-то использует анемичную модель, кто-то DDD, кто-то смесь и тд.

Dr10s Автор
12.12.2025 09:47Спасибо за отличный комментарий.
Я наоборот же в своей практике сталкивался с тем, что когда мы имеем контекст с большим доменом - класическая структура по типам начинает вставлять палки в колеса.
/domain /product /entity /service /value_object /event /another module/subdomainПредставьте, что product состоит из нескольких агрегатов и множества ентити, мы получим в папке entity много файлов и много подпапок. и это повториться в других папках: enity, value_object, event, service, exception и тд. И что бы понять какие у нас есть сущности и какие ивенты например эти сущности создают - нам придется пробежаться по каждой вложенной папке. На практике это зачастую приводило к тому что уже сделаны какие-то выводы по инвестигейту будущей работы и потом оказалось, что разработчик забыл посмотреть в какую-нибудь fooBar папку/подпапку и выбранное решение уже не подходит т.к. мы незаметили часть картины домена.
И чем больше контекст/домен - то болше будет вложенных папок/подпапок и собрать какое-то понимание того что происходит в домене становится затруднительно. Нам приходится закрывать папку domain и открывать папку application и уже изучать юзкейсы которых обычно еще больше и пропустить что-то становится еще более просто.
Так же про сабдомены:
/domain /product /another module/subdomainВ данной картине очень сложно понять кто рут домен а кто сапортящий. Приходится так же открывать каждый из них и изучать, а иногда становится не очевидно кто же первее "курица или яйцо". product или template.

Dr10s Автор
12.12.2025 09:47Так же про минусы - на моей практике внедрения данной структуры минус понимания типов (какие сущности у нас есть) существует только первый месяц +- пока команда не перестраивает своё мышление. Это нормально для всего чего-то нового и отличного от того как мы привыкли делать. Мозг лентяй и он часто сопротивляется.
С практикой применения данной структуры данные вопросы так же легко закрываются. Мы видим существительное и понимаем что это ентити или часть ентити. А solution architect обычно вообще не прибегает к коду и делегирует эти задачи команде разработки, он больше манипулирует другими вещами - такими как ендпоинты/схема бд и тд. А т.к. наши агрегаты/ентити это != схема бд здесь тоже можно начать заблуждаться в полученных выводах.
AlexViolin
Неужели хранение в одной папке событий TemplateChanged.php, TemplateArchivated.php и репозитория TemplateRepositoryInterface.php - это и есть результат многолетнего осмысления ддд?
Dr10s Автор
Да, все эти классы относятся к агрегату и являются его саппортом/результатом. Поэтому они должны лежать вместе. Согласитесь странно если кошелек лежит в одном кармане а деньги в другом только потому что это два разных типа предмета.
Когда начнёте смотреть на папки как на модули/неймспейсы/пакеты то становится более понятно.
AlexViolin
Каждый день смотрю на на модули/неймспейсы. Но хранить вместе события и репозитории никогда не буду. Но это конечно дело вкуса.
ivvi
Это не вопрос вкуса, а вопрос приверженности подходу DDD. Ознакомьтесь с ним хотя бы базово ;)
bravik
А не странно будет если в этом вашем кошельке у вас пачка денег, набор кредитных карт, водительское удостоверение и фотография жены с ребенком все будет лежать в одном кармане, а не каждый в своей секции?
Dr10s Автор
Устал уже отвечать на комментарии про "все в одну кучу". Это либо троли либо незнакомые с DDD люди. Пускай это будет последним ответом на это:
Здесь речь не о том что бы положить все в одну кучу, а о том что бы положить все по предметной области. На пример кошелек можно разделить на отделы:
Отдел для машины где лежит ВУ+полис и ТД
Отдел для спорта где лежат скидки и карты лояльности по спорту и ТД
Отдел для чаевых где лежат деньги которые тратятся только на чаевые.
Примеров можно привести кучу - вопрос фантазий, ниже в комнатах уже были хорошие примеры про кладбище и пирожки D
Здесь нет никакого "сложить все в кучу".
ИМХО - люди которые читают эту статью и видят только "сложить все в кучу" не имеют никакого представления о DDD. Ведь сама суть этого подхода - работа с предметной областью.
А когда вы сеньор помидор который всю жизнь кладет код по типам потому что увидел это в скелетоне какого нибудь фраемворка 20 лет назад и уже просто не может абстрагироваться от паттернов и думать в рамках единого языка и предметной области - у меня плохие новости: ни эта статья, ни ддд не для вас. Вы будете и дальше держать в голове бизнес а в коде паттерны и тратить колоссальное количество времени на паминг того что от вас хотят заказчики на то какими чудесными патернами вы это реализовали. А ещё большее время будут тратить коллеги работающие с вашим кодом что бы понять из как же паттернов предыдущий разработчик реализовал эту бизнес фитчу(по каким же папкам она разработана, что бы я посмотрел на каждый файлик и собрал в голове все воедино)