У першому матеріалі про момент, коли звичайного CRUD уже недостатньо я писала, що DDD стає корисним не через розмір проєкту, а через вагу правил.
Зі структурою директорій у мене була інша пастка: легко створити Domain/, Application/ і Infrastructure/ та відчути, що проєкт уже став domain-driven.
Але якщо контролер усе ще вирішує, чи можна активувати сутність, а загальний Service містить усі перевірки, змінилася лише адреса файлів.
Типова DDD-структура може виглядати переконливо
Product/├── Domain/│ ├── Entity/│ ├── ValueObject/│ └── Service/├── Application/│ ├── Command/│ └── Service/└── Infrastructure/ ├── Persistence/ └── Http/Тепер на code review я дивлюся не на дерево директорій, а на конкретну зміну стану.
Хто вирішує, чи дозволена операція? Де живе інваріант? Чи можна обійти правило прямим update()? Що станеться, якщо ту саму дію викликати не з HTTP?
Папка Domain не допомагає, якщо модель залишається анемічною
final class Product{ public string $status; public ?string $code; public ?string $name;} final class ProductService{ public function activate( Product $product, ): void { if ($product->status !== 'draft') { throw new LogicException(); } Можна покласти ProductService у Domain/Service, але правило від цього не стане краще змодельованим.
Product усе ще дозволяє будь-кому встановити status = active напряму. Проблема не в назві папки, а у власнику поведінки.
Я починаю з власника правила, а не з namespace
У матеріалі про те, де має жити бізнес-правило я розбирала саме це питання: controller, service чи domain model?
Якщо Product має сам знати, чи може перейти в новий стан, я хочу побачити це в Product. Якщо правило належить значенню, можливо, потрібен Value Object. Якщо воно справді охоплює кілька доменних понять без одного власника, тоді може бути доречний Domain Service.
Коли правило живе в моделі, структура починає щось означати
final class Product{ public function activate(): void { if ( $this->status !== ProductStatus::DRAFT ) { throw new DomainException( 'Product cannot be activated.', ); } if (! $this->isComplete()) { throw new DomainException( 'Product is incomplete.',Application layer не повинен ставати новою папкою для всієї логіки
У матеріалі про Application Service і Domain Service я писала, що Application Service координує use case: транзакцію, repository, зовнішні залежності та порядок кроків.
Якщо після переходу на "DDD-структуру" весь код просто переїхав із app/Services у Application/Services, відповідальність не змінилася.
Infrastructure потрібен для технічних деталей, а не для бізнес-сенсу
Domain:Що є валідним? Application:Як ми виконуємо сценарій використання (use case)? Infrastructure:Як ми взаємодіємо з:- базою даних- чергою- файловою системою- зовнішнім APIНавіть правильні шари не визначають межі домену
Глобальні Domain/, Application/ і Infrastructure/ ще не означають, що bounded context знайдений.
У матеріалі про те, як я визначаю межі домену в системі, що росте я писала, що межі стають видимими через різні значення слів, життєві цикли, інваріанти та причини змін.
Папки можуть відобразити вже знайдену межу, але не можуть знайти її замість нас.
Іноді краще почати простіше й дати структурі вирости
На початковому етапі: Product/ Product.php ActivateProduct.php ProductRepository.php Згодом, коли того вимагатиме складність: Product/ Domain/ Application/ Infrastructure/Я не вважаю пласку структуру недоліком, якщо відповідальності зрозумілі.
Шари варто вводити тоді, коли вони допомагають орієнтуватися в уже наявній складності, а не лише створюють видимість архітектури.
Тест доменного правила говорить більше за назву папки
public function test_incomplete_product_cannot_be_activated(): void{ $product = Product::draft( code: null, name: 'Example', ); $this->expectException( DomainException::class, ); $product->activate();}У AVN Center структура стала для мене наслідком моделі
У роботі над AVN Center для мене стало важливо не починати з питання "які папки нам потрібні?".
Кориснішими виявилися питання про правила, власників рішень, межі та різницю між координацією сценарію і доменним рішенням.
Після цього структура стає природнішою, бо відображає вже зрозумілі відповідальності. Внутрішню карту модулів AVN Center тут навмисно не показую.
Як я перевіряю, чи це справді DDD, а не тільки папки
Я дивлюся на кілька сигналів:
- Чи важливі бізнес-правила видно в доменній моделі?
- Чи entity захищає допустимий стан, а не лише зберігає поля?
- Чи Application Service координує сценарій замість володіння всіма правилами?
- Чи Infrastructure містить технічні деталі, а не предметний сенс?
- Чи межі модулів випливають із мови, інваріантів і життєвих циклів?
- Чи папки пояснюють відповідальності, а не замінюють їх?
Папки мають відображати модель, а не підміняти її
Я не відмовилася від Domain/, Application/ чи Infrastructure/. Вони корисні, коли відповідальності вже зрозумілі.
Але тепер я не починаю DDD з дерева директорій.
Спочатку я шукаю правила, інваріанти, власників рішень і межі. Потім структура допомагає зробити ці рішення видимими в коді.
DDD не починається з папки Domain. Воно починається тоді, коли предметні правила перестають бути випадковими if у контролерах і стають частиною моделі.



