Трилемма DDD: выбираем оптимальный вариант

Трилемма DDD — нельзя одновременно максимизировать все три цели. Придётся выбирать приоритеты или искать компромисс в архитектуре.

В контексте Laravel и модульного монолита три вершины обычно формулируют так:

                    ┌─────────────────────┐
                    │   СТРОГИЙ DOMAIN    │
                    │  инварианты, VO,    │
                    │  rich model, чистые │
                    │     зависимости     │
                    └──────────┬──────────┘
                               │
              «все три сразу   │   «быстро и
               — иллюзия»      │   правильно»
                               │
         ┌─────────────────────┼─────────────────────┐
         │                     │                     │
         ▼                     ▼                     ▼
┌─────────────────┐   ┌───────────────┐   ┌─────────────────┐
│ СКОРОСТЬ        │   │  КОМПРОМИСС   │   │ ПРОИЗВОДИ-      │
│ РАЗРАБОТКИ      │◄──│  модульный    │──►│ ТЕЛЬНОСТЬ       │
│ Laravel-way,    │   │  монолит +    │   │ CQRS read-модель│
│ меньше слоёв    │   │  CQRS-lite    │   │ join, DTO, кэш  │
└─────────────────┘   └───────────────┘   └─────────────────┘

Выберите любые две вершины — третья пострадает. Задача архитектора — осознанно решить, где и когда платить цену.

* * *

Бизнес-правила живут в сущностях и Value Objects. Инфраструктура не просачивается. Инварианты невозможно обойти.

final class Order
{
    private function __construct(
        private OrderId $id,
        private OrderStatus $status,
        private Money $total,
    ) {}

    public static function place(OrderId $id, Money $total): self
    {
        if ($total->isZero()) {
            throw new OrderTotalMustBePositive();
        }
        return new self($id, OrderStatus::placed(), $total);
    }

    public function cancel(): void
    {
        if ($this->status->isCancelled()) {
            throw new OrderAlreadyCancelled();
        }
        $this->status = OrderStatus::cancelled();
    }
}

Цена: больше кода, Mapper'ы, медленнее старт фичи. Выигрыш: баги ловятся в домене, а не в проде.

* * *

Laravel-way: Eloquent в контроллере, быстрые CRUD, минимум абстракций. Идеально для MVP и прототипов.

class OrderController extends Controller
{
    public function store(Request $request)
    {
        $order = Order::create($request->validated());
        return response()->json(['id' => $order->id], 201);
    }

    public function cancel(string $id)
    {
        Order::findOrFail($id)->update(['status' => 'cancelled']);
        return response()->noContent();
    }
}

Цена: анемичная модель, правила размазаны по контроллерам и валидации. Отменить заказ дважды? Возможно. Выигрыш: фича за час.

* * *

Оптимизированные read-запросы: join'ы, DTO, кэш, денормализация. Write и read разделены (CQRS-lite).

// Application/Query/OrderReadRepository — возвращает DTO, не Entity
final readonly class EloquentOrderReadRepository implements OrderReadRepository
{
    public function listForDashboard(OrderListCriteria $c): LengthAwarePaginator
    {
        return DB::table('orders')
            ->join('users', 'users.id', '=', 'orders.user_id')
            ->select(['orders.id', 'orders.total', 'users.name as customer'])
            ->where('orders.status', 'placed')
            ->paginate($c->perPage);
    }
}

Цена: две модели (write Entity + read DTO), рассинхрон при неаккуратном CQRS. Выигрыш: список из 100k заказов — миллисекунды.

* * *

ПараПолучаетеЖертвуете
Domain + СкоростьПравильная логика, быстрый стартПроизводительность read-слоя
Domain + ПроизводительностьИнварианты + быстрые спискиВремя на Mapper, Handler, тесты
Скорость + ПроизводительностьБыстро пишете и читаетеЧистота домена, техдолг
* * *

  • Domain — строгий внутри модуля, но без фанатизма (прагматичные исключения документируйте).
  • CQRS-lite — write через Entity, read через DTO. Одна БД, разные пути.
  • Тонкий Presentation — Action за 10 строк, Handler переиспользуется из API, MCP, Console.
  • PHPArkitect — автоматически ловит нарушения слоёв, не полагайтесь на дисциплину.
  • Не микросервисы — распределённая трилемма (consistency / autonomy / latency) вас пока не касается.

// Write — доменные правила
final readonly class CancelOrderHandler
{
    public function handle(CancelOrderCommand $cmd): void
    {
        $order = $this->repository->getById($cmd->orderId);
        $order->cancel(); // инвариант внутри Entity
        $this->repository->save($order);
    }
}

// Read — производительность
final readonly class ListOrdersHandler
{
    public function handle(ListOrdersQuery $q): LengthAwarePaginator
    {
        return $this->readRepository->paginate($q->criteria); // плоский DTO
    }
}
* * *

Трилемма DDD — не повод отказаться от DDD, а повод осознанно выбирать. Модульный монолит с CQRS-lite позволяет не жертвовать Domain ради скорости и не жертвовать производительностью ради чистоты — платите сложностью архитектуры, но она окупается на горизонте месяцев, а не дней.

Пост создан через MCP · Editor.js

Комментарии

Загрузка…

Войдите, чтобы оставить комментарий