Трилемма 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
Комментарии
Войдите, чтобы оставить комментарий