АналитикаСистемный анализСистемный аналитик

Разберите ситуацию: заказ и его позиции иногда расходятся после параллельных изменений. Как определить гран...

Разберите ситуацию: заказ и его позиции иногда расходятся после параллельных изменений. Как определить границу агрегата?

Проходите собеседования с ИИ помощником Hintsage

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

Границу агрегата определяют по инвариантам, которые должны сохраняться атомарно. Если статус заказа и его позиции нельзя считать корректными по отдельности, их следует включить в один агрегат с заказом в роли корня. Связанные данные, для которых допустима временная рассогласованность, должны находиться за пределами этого агрегата.

Исторический контекст

Понятие агрегата сформировалось в рамках предметно-ориентированного проектирования как способ управлять сложностью доменной модели. Исходная проблема состояла в том, что попытка изменять весь связанный граф объектов одной транзакцией ухудшала производительность, масштабируемость и понимание правил предметной области.

Агрегат вводит явную границу согласованности: внутри неё бизнес-инварианты поддерживаются синхронно, а внешние объекты взаимодействуют с агрегатом через его корень. Это позволяет отделить обязательную атомарность от обычной связанности данных.

Постановка проблемы

Заказ может содержать статус, позиции, итоговую сумму и ограничения на изменение. При параллельных операциях один процесс способен изменить статус, пока другой меняет состав заказа. Если система не определила границу согласованности, итоговое состояние может нарушить бизнес-правило: например, оплаченный заказ окажется без обязательной позиции.

Ошибкой будет считать агрегатом всё, что связано с заказом в базе данных. Тогда изменение клиента, доставки или платежа может необоснованно блокировать заказ либо требовать общей распределённой транзакции. Обратная ошибка — разделить объекты, между которыми существует обязательный инвариант, и тем самым сделать корректное состояние недостижимым.

Подробное решение

Сначала необходимо выписать инварианты предметной области. Например: оплаченный заказ нельзя изменить; итоговая сумма равна сумме его позиций; позиция не может существовать без заказа. Затем нужно определить, какие из этих правил должны проверяться в одной операции изменения.

Если правило требует атомарности заказа вместе с его позициями, заказ становится корнем агрегата, а позиции — внутренними сущностями. Внешний код не должен напрямую менять позицию в обход корня: он передаёт заказу команду, а агрегат проверяет инварианты и изменяет своё состояние целиком.

Остальные сущности не обязаны входить в тот же агрегат только из-за ссылки на заказ. Например, платёж может быть отдельным агрегатом: его состояние меняется по собственным правилам, а заказ получает результат через событие или отдельную команду. В таком случае между агрегатами возможна согласованность в конечном счёте, но бизнес-требования должны явно допускать короткое переходное состояние.

Для защиты от параллельных изменений обычно применяют версию агрегата или другой механизм обнаружения конфликта. Если версия изменилась после чтения, операция отклоняется или повторяется с актуальным состоянием; молчаливое затирание чужих изменений недопустимо.

Граница агрегата не обязана совпадать с таблицей или физической транзакцией базы данных. Она является прежде всего границей доменной согласованности, хотя часто влияет на структуру хранения и размер транзакции. Слишком крупный агрегат увеличивает блокировки, конфликты и стоимость чтения; слишком мелкий переносит сложные инварианты в ненадёжную координацию между сервисами.

Ситуация из практики

В интернет-магазине заказ мог одновременно перейти в статус «оплачен» и получить удалённую позицию. Рассматривались три варианта. Первая стратегия — блокировать все связанные записи на время любой операции; она давала сильную согласованность, но ухудшала параллельность и усложняла интеграции. Вторая — сделать заказ, позиции и платёж одним большим агрегатом; это упростило бы атомарные проверки, но чрезмерно увеличило бы область блокировок.

Третья стратегия — оставить заказ вместе с позициями одним агрегатом, а платёж выделить отдельно. Изменение состава заказа проверяло версию заказа, а подтверждение платежа инициировало переход заказа через отдельную команду. Выбрали этот вариант, потому что правила заказа требовали немедленной согласованности с позициями, тогда как платёж имел самостоятельный жизненный цикл.

После внедрения конфликтующие изменения перестали затирать друг друга: одна операция получала отказ по устаревшей версии. Переход между состояниями заказа и платежа стал явно описанным процессом, а не побочным эффектом общей транзакции.

Что кандидаты часто упускают

  1. Должны ли все связанные сущности входить в один агрегат?

Нет. Связь, внешний ключ или совместное отображение на экран не доказывают необходимость общей атомарности. В один агрегат включают объекты, между которыми существуют инварианты, требующие проверки в рамках одной операции. Для остальных объектов достаточно ссылаться на идентификатор агрегата и взаимодействовать через публичные операции или события.

  1. Почему агрегат не следует проектировать по границам таблиц базы данных?

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

  1. Что делать, если один бизнес-процесс меняет несколько агрегатов?

Нужно разделить обязательные инварианты и последовательность бизнес-шагов. Если правило действительно требует атомарного изменения, границы агрегатов, вероятно, выбраны неверно либо требуется специализированная координация. Если временная рассогласованность допустима, процесс можно реализовать через команды, события, повторные попытки и компенсацию, явно описав промежуточные состояния и последствия сбоя.