Domain-Driven Design виправданий тоді, коли складність продукту зосереджена в бізнес-правилах. Ознаки: правила мають винятки й змінюються разом із бізнесом, експерти з різних відділів вкладають різне значення в одні слова, помилка в логіці коштує грошей, продукт житиме роками. У такому середовищі модель, яка говорить мовою бізнесу, окупає себе: зміну вимоги можна знайти в коді по назві, а інваріанти живуть в одному місці.
Найкорисніша частина DDD стратегічна. Bounded context — це межа, всередині якої слово має одне значення. Product у каталозі, на складі й у замовленні означає різні речі, і спроба зробити одну модель на всіх дає гігантський клас, який ніхто не розуміє. Три невеликі моделі з явними інтеграціями через події простіші, навіть якщо тактичних патернів у них мінімум.
Шкодить DDD там, де складності немає. CRUD-адмінка, звіти, інтеграційний сервіс, який перекладає один JSON в інший, не потребують агрегатів і репозиторіїв: пʼять класів на одну форму лише сповільнюють команду. Так само шкодить підміна суті формою: папки Domain, Application, Infrastructure без моделі та мови це не DDD, а бюрократія.
Зріла відповідь містить обидва приклади: контекст, де модель окупилась, і контекст, де від DDD свідомо відмовились на користь транзакційного скрипта й Eloquent. DDD можна застосовувати до одного складного контексту всередині звичайного застосунку, і саме так він найчастіше працює.
// Складний контекст: правила живуть у моделі, мова та сама, що в бізнесу
final class Subscription
{
private function __construct(
public private(set) SubscriptionId $id,
private Plan $plan,
private Period $period,
private SubscriptionStatus $status,
) {}
public function renew(Clock $clock): void
{
if ($this->status === SubscriptionStatus::Cancelled) {
throw new CannotRenewCancelledSubscription($this->id);
}
if ($this->period->endsAfter($clock->now()->modify('+14 days'))) {
throw new RenewalTooEarly($this->id, $this->period); // правило з договору, не з таблиці
}
$this->period = $this->period->extendBy($this->plan->billingCycle());
$this->record(new SubscriptionRenewed($this->id, $this->period));
}
}
// Простий контекст поруч: CRUD без агрегатів, і це свідомий вибір
final class TagController
{
public function store(Request $request): RedirectResponse
{
Tag::create($request->validate(['name' => ['required', 'max:50']]));
return back();
}
}
// Межа контекстів: той самий «продукт», різні моделі, інтеграція через подію
// Catalog\Product — опис, фото, SEO
// Warehouse\StockItem — sku, залишок, комірка
// Orders\OrderLine — назва й ціна, зафіксовані на момент покупки
Найсильніша відповідь — приклад, де ви свідомо не застосували DDD і чому це було правильно. Другий за силою: приклад слова, яке в двох контекстах означало різне.