Какие проблемы появляются при делении агрегата в Domain-Driven Design?

«Какие проблемы появляются при делении агрегата в Domain-Driven Design?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В DDD агрегат — это кластер связанных объектов, рассматриваемых как единое целое с корнем (Aggregate Root), который контролирует доступ и гарантирует инварианты. Разделение одного агрегата на несколько — это серьёзное архитектурное решение, которое влечёт за собой проблемы:

  1. Нарушение транзакционной целостности: Изменения, которые раньше были атомарными в рамках одного агрегата, теперь требуют обновления нескольких агрегатов. Это либо приводит к необходимости использования распределённых транзакций (сложных и медленных), либо к согласованности в конечном счёте (eventual consistency) через доменные события.

  2. Усложнение поддержки инвариантов: Бизнес-правило (инвариант), которое раньше проверялось внутри одного агрегата, теперь может затрагивать несколько. Его обеспечение требует координации, часто через обработку доменных событий или использование паттерна Saga.

    • Было: В агрегате Order можно было гарантировать, что сумма всех OrderItem не превышает кредитный лимит пользователя.
    • Стало: После выделения OrderItem в отдельный агрегат это правило нужно проверять в сервисе домена, который координирует оба агрегата.
  3. Ухудшение производительности при загрузке: Чтобы выполнить операцию, теперь может потребоваться загрузить несколько агрегатов вместо одного, увеличивая число запросов к БД.

  4. Возрастание сложности: Архитектура становится сложнее из-за появления новых сущностей, событий и процессов согласования. Это увеличивает порог входа для новых разработчиков.

Пример проблемы:

// Монолитный агрегат Заказ (просто, но может быть большим)
class Order {
    private UUID id;
    private List<OrderLine> lines; // Value Objects внутри агрегата
    private Money total;
    // Инвариант: total = sum(lines.price) поддерживается внутри
    public void addLine(Product product, int quantity) {
        // Проверка лимитов, пересчёт total — всё здесь
    }
}

// После разделения (гибче, но сложнее)
class Order { // Агрегат 1
    private UUID id;
    private Money total;
}
class OrderLine { // Агрегат 2
    private UUID orderId;
    private ProductId productId;
    private Money price;
}
// Теперь для добавления товара нужно:
// 1. Загрузить Order.
// 2. Создать OrderLine (отдельная транзакция).
// 3. Отправить событие OrderLineAdded.
// 4. Обработать событие и пересчитать total в Order (ещё одна транзакция).

Решение о разделении должно быть взвешенным и оправдываться требованиями масштабируемости или независимой жизненной цикличности частей домена.