> Есть ли опыт работы с Domain-Driven Design (DDD)? (PHP)

Уровень: middle · Роль: backend · Язык: PHP · Категория: Технические вопросы

Компании: Travelata

Стек: PHP

> Пример ответа

Короткий ответ

Да, есть практический опыт применения DDD в PHP-проектах. Работал с такими концепциями, как bounded context, aggregate, domain event, repository и application service. Использовал DDD в проектах со сложной бизнес-логикой, где важно было отделить домен от инфраструктуры. Основной фокус - на моделировании бизнес-процессов в коде и поддержании инвариантов внутри агрегатов.

Подробное объяснение

DDD - это подход к проектированию, где основное внимание уделяется доменной логике, а не техническим деталям. В PHP это особенно актуально, потому что язык часто используется для быстрых CRUD-приложений, и без явного выделения домена логика размазывается по контроллерам и сервисам.

В своей работе я применял следующие элементы DDD:

  • Bounded context - выделял границы между разными подсистемами (например, заказы и каталог товаров), чтобы избежать смешения моделей.
  • Aggregate - проектировал корневые сущности, которые управляют инвариантами. Например, Order как aggregate root, который контролирует изменение позиций заказа.
  • Domain event - использовал для реакции на изменения внутри домена, например, OrderPlaced, чтобы запускать сайд-эффекты в других контекстах.
  • Repository - инкапсулировал доступ к данным, чтобы домен не зависел от ORM или конкретной БД.
  • Application service - организовывал сценарии использования (use cases) и транзакции, оставляя домен чистым.

Важный trade-off: DDD добавляет сложность в структуру проекта. Поэтому я применял его не везде, а только там, где бизнес-логика действительно сложная и есть несколько взаимосвязанных правил. Для простых CRUD-модулей DDD избыточен.

На практике

В одном из проектов (система управления подписками) я использовал DDD для моделирования процесса продления подписки. Был bounded context Billing, внутри которого агрегат Subscription управлял состоянием: активна, просрочена, отменена. Все изменения состояния проходили через методы агрегата, что гарантировало соблюдение инвариантов - например, нельзя отменить уже отменённую подписку.

Инфраструктура (Eloquent, очереди, внешние платёжные API) была вынесена за пределы домена. Домен общался с внешним миром через интерфейсы, а адаптеры реализовывались в инфраструктурном слое. Это позволяло тестировать домен без БД и без реальных HTTP-запросов.

Также использовал domain events для интеграции с другими контекстами: после успешной оплаты публиковалось событие PaymentReceived, на которое подписывался контекст Notifications для отправки письма клиенту.

Пример кода

PHP
// Domain layer - Aggregate Root
final class Subscription
{
private function __construct(
private readonly SubscriptionId $id,
private SubscriptionStatus $status,
private \DateTimeImmutable $expiresAt,
) {}
public static function create(SubscriptionId $id, \DateTimeImmutable $expiresAt): self
{
$subscription = new self($id, SubscriptionStatus::ACTIVE, $expiresAt);
$subscription->recordEvent(new SubscriptionCreated($id));
return $subscription;
}
public function renew(\DateTimeImmutable $newExpiryDate): void
{
if ($this->status === SubscriptionStatus::CANCELLED) {
throw new \DomainException('Cannot renew cancelled subscription');
}
$this->expiresAt = $newExpiryDate;
$this->recordEvent(new SubscriptionRenewed($this->id, $newExpiryDate));
}
public function cancel(): void
{
if ($this->status === SubscriptionStatus::CANCELLED) {
throw new \DomainException('Subscription already cancelled');
}
$this->status = SubscriptionStatus::CANCELLED;
$this->recordEvent(new SubscriptionCancelled($this->id));
}
}
// Application layer - Use case
final readonly class RenewSubscriptionService
{
public function __construct(
private SubscriptionRepository $repository,
private EventDispatcher $dispatcher,
) {}
public function renew(SubscriptionId $id, \DateTimeImmutable $newExpiryDate): void
{
$subscription = $this->repository->findById($id);
$subscription->renew($newExpiryDate);
$this->repository->save($subscription);
$this->dispatcher->dispatch($subscription->releaseEvents());
}
}

Как отвечать на собеседовании

Начни с конкретного примера проекта, где DDD реально применялся. Опиши, какую бизнес-задачу решал, какие bounded context выделил и почему. Покажи понимание trade-off: где DDD оправдан, а где - нет. Упомяни, что DDD - это не про технологии, а про моделирование домена, и что PHP-фреймворки (Laravel, Symfony) не навязывают DDD, но позволяют его реализовать.

Если спросят про сложности - честно скажи, что основная сложность в дисциплине: не тащить инфраструктурные детали в домен и не делать агрегаты слишком большими. Также можно упомянуть, что DDD хорошо сочетается с CQRS и event sourcing, но это не обязательные спутники.

Что проверяет интервьюер

Интервьюер проверяет:

  • понимаешь ли ты разницу между domain layer, application layer и infrastructure layer;
  • умеешь ли проектировать агрегаты и соблюдать инварианты;
  • знаешь ли, когда DDD уместен, а когда - избыточен;
  • можешь ли объяснить, как repository и domain events вписываются в общую архитектуру;
  • насколько ты критичен к своему опыту - не утверждаешь ли, что используешь DDD везде подряд.

Типичные ошибки

  • Говорить, что DDD - это просто слои в папках (Domain, Application, Infrastructure). Это лишь следствие, а не суть.
  • Путать aggregate root с обычной Entity. Агрегат управляет целостностью группы объектов, а не просто является моделью из БД.
  • Использовать DDD для простого CRUD - это создаёт лишнюю сложность без пользы.
  • Не упоминать bounded context. Без него DDD превращается в "одну большую модель на всё приложение", что приводит к проблемам.
  • Показывать код, где домен зависит от Eloquent или других инфраструктурных классов - это нарушение принципов DDD.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью