0% прочитано

DDD на практиці: коли звичайний CRUD перестає бути достатнім

CRUD чудово працює, поки сутність справді проста. Але коли з’являються стани, дозволені переходи, залежності та правила, контролер починає знати занадто багато. На простих прикладах показую, коли модель предметної області стає кориснішою за набір create/update/delete.

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

Коли я починала працювати з DDD, найбільш незрозумілим було не саме поняття. Було складно відчути момент, коли воно справді потрібне.

Звичайний CRUD довго виглядає достатнім. Є форма, контролер, модель, кілька полів у базі. Створили запис, прочитали, змінили, видалили.

Проблема починається не тоді, коли проєкт стає великим. Вона починається тоді, коли запис перестає бути просто набором полів і навколо нього з’являються правила.

Саме це я побачила під час роботи над AVN Center. У попередньому практичному матеріалі про DDD я вже показувала один окремий приклад. Тут хочу пояснити сам перехід від CRUD до моделі предметної області просто і без зайвої теорії.

DDD тут означає проєктування навколо предметної області. Я починаю не з папок і шаблонів, а з питання: які правила система повинна захищати?

CRUD сам по собі не є проблемою

Для простої сутності цього може бути достатньо
public function update(    UpdateProductRequest $request,    Product $product,): RedirectResponse {    $product->update(        $request->validated(),    );     return back();}

Я не намагаюся замінити CRUD лише тому, що DDD звучить складніше або "архітектурніше".

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

Зайва абстракція в такому місці лише зробить код важчим.

Проблема починається, коли update вже не означає просто змінити поля

Запис поступово перетворюється на поведінку
Спочатку: namedescriptionstatus  Згодом: - статус може змінюватися лише у визначеній послідовності- для активації потрібні повні дані- деякі поля стають незмінними після активації- виконання однієї дії залежить від певного стану- видалення дозволено не завжди

У AVN Center я побачила саме такий розвиток.

Спочатку об’єкт можна було уявити як звичайний запис із кількома полями. Потім з’явилися стани, допустимі переходи, залежності між діями та умови, за яких певна операція взагалі дозволена.

Я навмисно не описую тут внутрішню карту модулів або точні правила продукту. Для цієї статті важливий сам досвід: коли правила накопичуються, вони вже описують не таблицю, а зміст предметної області.

Перший сигнал для мене: контролер починає знати занадто багато

Контролер уже вирішує питання предметної області
public function activate(    Product $product,): RedirectResponse {    if ($product->status !== 'draft') {        abort(422);    }     if (        blank($product->name)        || blank($product->code)    ) {        abort(422);    }     if ($product->is_blocked) {        abort(422);

Такий код ще не виглядає страшно. Але він уже відповідає на важливі питання.

З якого стану можна активувати об’єкт? Які дані обов’язкові? Яка умова блокує дію?

Це не питання HTTP. Це правила предметної області.

Якщо та сама дія з’явиться в команді, черзі, адміністративній частині або іншому сценарії, ці перевірки доведеться або копіювати, або виносити в окреме місце.

Я хочу, щоб об’єкт сам знав, що означає активувати

Правило живе поруч із поведінкою, яку захищає
final class Product{    public function activate(): void    {        if ($this->status !== ProductStatus::DRAFT) {            throw new DomainException(                'Product cannot be activated from this state.',            );        }         if (! $this->isReadyForActivation()) {            throw new DomainException(                'Product is not ready for activation.',            );        } 

Тепер зовнішній код не вирішує, які саме поля або стани роблять активацію допустимою.

Він лише просить об’єкт виконати дію.

Модель предметної області або виконує коректний перехід, або відмовляє.

Для мене це одна з найкорисніших змін у сприйнятті DDD: замість "оновити status" я думаю "виконати дію, яка має правила".

Прикладний сервіс координує сценарій, але не вигадує правило

Координація залишається зовні, правило - у моделі
public function handle(    ProductId $id,): void {    DB::transaction(function () use ($id) {        $product = $this->products            ->getForUpdate($id);         $product->activate();         $this->products->save(            $product,        );    });}

Зовнішній сервіс робить те, що йому природно: відкриває транзакцію, завантажує стан, викликає дію і зберігає результат.

Але він не містить список умов, за яких активація дозволена.

Це проста, але важлива межа: координація сценарію і правило предметної області - не одне й те саме.

Найважливіше для мене тут - правило, яке не можна порушити

У DDD для такого правила часто використовують слово "інваріант". Простими словами, це умова, яка повинна залишатися правдивою після будь-якої дозволеної дії.

Наприклад: активний об’єкт повинен мати обов’язкові дані. Або певний перехід стану не можна виконати двічі.

Коли такі правила з’являються, я вже не хочу покладатися на те, що кожен контролер не забуде потрібну перевірку.

Частину правил також корисно підсилювати нижче. Про це я окремо писала в матеріалі про обмеження бази даних як останній захисний шар.

Статус перестає бути просто рядком у таблиці

Стани мають зміст, а переходи мають правила
draft  |  vready  |  vactive  |  vsuspended  Не кожен перехіддозволений в обох напрямках.

У простому CRUD поле status легко сприймати як звичайний рядок.

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

Тому замість довільного update(["status" => ...]) я віддаю перевагу явним діям на кшталт activate(), suspend() або restore(), якщо ці дії справді мають окремі правила.

Але не кожна умова означає, що потрібен DDD

Перевірка довжини рядка, формату електронної адреси або простих вхідних даних ще не означає, що потрібна окрема модель предметної області.

Я дивлюся не на кількість if, а на їхній зміст.

Якщо умова описує правило предметної області і повинна бути однаковою незалежно від того, звідки викликана дія, це вже сильний сигнал.

DDD не починається зі структури папок

Такі папки самі по собі нічого не гарантують
Domain/Application/Infrastructure/Http/

Можна створити красиву структуру Domain/Application/Infrastructure і все одно залишити всі правила в контролерах.

І навпаки: можна почати з маленької моделі, де одна важлива дія і її правила зібрані разом, і вже отримати користь.

Тому я зараз починаю не з питання "які папки створити?", а з питання "яке правило система не повинна дозволити порушити?".

Одна дія не означає, що все треба затягнути в один великий сценарій

DDD також не означає, що одна бізнес-дія повинна затягнути в себе всі побічні процеси.

Я намагаюся окремо бачити те, що має бути узгодженим прямо зараз, і те, що може відбутися після успішної зміни стану.

Це пов’язано з іншою практичною темою, про яку я вже писала: коли одна транзакція краща за подію.

Мій практичний поріг: коли я перестаю залишати логіку в CRUD

Я починаю серйозно дивитися в бік моделі предметної області, коли бачу кілька ознак одночасно:

  • стан має зміст, а не просто зберігається в полі;
  • не всі переходи між станами дозволені;
  • одна й та сама дія викликається з кількох місць;
  • для дії потрібні правила, які не стосуються HTTP або форми;
  • після помилки система може опинитися в недопустимому стані;
  • розробнику вже важко зрозуміти правило, дивлячись лише на контролер;
  • назва конкретної дії пояснює намір краще за універсальний update().

Коли я все ще залишаю звичайний CRUD

CRUD для мене залишається правильним вибором, якщо:

  • об’єкт переважно зберігає дані;
  • поля змінюються незалежно;
  • немає важливих переходів стану;
  • немає правил, які треба однаково захищати в кількох сценаріях;
  • окрема модель поведінки не зробить код зрозумілішим.

Для мене це теж частина DDD-підходу: не моделювати складність там, де її немає.

Що робота над AVN Center змінила в моєму підході

Раніше я частіше дивилася на нову функцію через структуру таблиць і список потрібних операцій.

Тепер у складніших частинах системи я спочатку питаю:

  • який стан тут допустимий;
  • які переходи дозволені;
  • яке правило повинно виконуватися завжди;
  • хто може змінювати цей стан;
  • що має відбутися в одній узгодженій операції, а що може відбутися пізніше.

Ці питання не змушують автоматично будувати складну архітектуру. Але вони дуже швидко показують, де простого CRUD уже недостатньо.

DDD стає корисним не через розмір проєкту, а через вагу правил

Для мене межа між CRUD і DDD тепер досить проста.

Поки система переважно зберігає дані, CRUD працює чудово.

Коли система починає захищати правила, стани та допустимі переходи, мені вже недостатньо просто оновлювати поля.

Тоді з’являється модель предметної області, яка говорить мовою дій: активувати, призупинити, підтвердити, відхилити, завершити.

DDD для мене починається не з архітектурної схеми. Воно починається в момент, коли важливіше зберегти правило, ніж просто зберегти запис.

Якщо контролер уже пояснює, що означає стан і коли дозволена дія, варто запитати: чи не належить це правило самій предметній області?