Практична серія про Domain-Driven Design у Laravel: межі домену, агрегати, бізнес-правила, сервіси, транзакції, події та моделі читання на прикладах із реального проєкту.
CRUD чудово працює, поки сутність справді проста. Але коли з’являються стани, дозволені переходи, залежності та правила, контролер починає знати занадто багато. На простих прикладах показую, коли модель предметної області стає кориснішою за набір create/update/delete.
Межі предметної області рідко видно ідеально на початку. На практиці я дивлюся на мову, правила, причини змін і життєвий цикл. Якщо дві частини системи змінюються з різних причин, це сильний сигнал, що їх не варто тримати в одному контексті.
Зовнішній ключ ще не означає, що дві сутності мають змінюватися як одне ціле. На практиці aggregate визначається правилами, які потрібно захистити в одній узгодженій зміні, а не схемою таблиць.
Спочатку бізнес-правило легко залишити в контролері або прикладному сервісі. Але коли воно використовується з кількох сценаріїв, стає видно, що це вже правило предметної області. На простих прикладах показую, як я розділяю перевірку запиту, координацію сценарію і доменний інваріант.
Мені довго було легко назвати будь-яку складнішу логіку сервісом. На практиці допомогло просте розділення: прикладний сервіс координує сценарій, транзакцію, сховище та зовнішні залежності, а доменний сервіс виражає правило предметної області, яке природно не належить одному об’єкту.
Одна бізнес-операція не завжди означає одну велику транзакцію. На практиці я відділяю те, що повинно змінитися узгоджено в межах одного aggregate, від побічних дій, які можуть виконатися після commit.
На прикладі ProductRegistry показую кілька практичних рішень DDD: як розділяю відповідальність між шарами, чому ProductRegistration і BootstrapInvitation залишаються окремими агрегатами, коли доречні події та коли потрібна сильна узгодженість.
Я не використовую доменні події всюди. Якщо друга дія є обов’язковою частиною інваріанта, залишаю її в прямому виклику й одній транзакції. Якщо інша частина системи лише реагує на вже збережений факт, доменна подія часто підходить краще.
Модель предметної області потрібна насамперед для захисту правил під час зміни стану. Для списку, таблиці або відповіді API часто потрібна інша форма даних. Пояснюю, чому окрема модель читання не порушує DDD, а часто робить код простішим.
Рядок достатній, поки значення не має власних правил. Коли разом із ним з’являються нормалізація, перевірка, порівняння або окрема семантика, Value Object допомагає зробити ці правила явними й не розкидати їх по сервісах.
Папки Domain, Application та Infrastructure можуть допомогти організувати код, але самі по собі не створюють DDD. Якщо бізнес-правила досі живуть у контролерах і загальних сервісах, змінилася лише структура директорій.
Репозиторій не потрібен для кожної Eloquent-моделі. У DDD він має сенс як межа збереження aggregate або коли прикладний рівень не повинен знати деталі збереження.
Якби я починала DDD-проєкт знову, я б менше намагалася спроєктувати все наперед. Раніше фіксувала б інваріанти, обережніше вводила abstraction, чіткіше відділяла читання і менше дозволяла таблицям диктувати форму доменної моделі.