0% прочитано

DDD не починається з папок: чому структура директорій ще не робить проєкт domain-driven

Папки Domain, Application та Infrastructure можуть допомогти організувати код, але самі по собі не створюють DDD. Якщо бізнес-правила досі живуть у контролерах і загальних сервісах, змінилася лише структура директорій.

17 жовтня 2026 р. 11 хв читанняЛаравель

У першому матеріалі про момент, коли звичайного CRUD уже недостатньо я писала, що DDD стає корисним не через розмір проєкту, а через вагу правил.

Зі структурою директорій у мене була інша пастка: легко створити Domain/, Application/ і Infrastructure/ та відчути, що проєкт уже став domain-driven.

Але якщо контролер усе ще вирішує, чи можна активувати сутність, а загальний Service містить усі перевірки, змінилася лише адреса файлів.

Структура директорій може підтримувати DDD, але не може замінити модель. Папка показує, де лежить код. Вона не показує, хто володіє бізнес-правилом.

Типова 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/

Я не вважаю пласку структуру недоліком, якщо відповідальності зрозумілі.

Шари варто вводити тоді, коли вони допомагають орієнтуватися в уже наявній складності, а не лише створюють видимість архітектури.

Тест доменного правила говорить більше за назву папки

Правило можна перевірити без HTTP і без application service
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 у контролерах і стають частиною моделі.