Разберите ситуацию: заказ и его позиции иногда расходятся после параллельных изменений. Как определить границу агрегата?
Границу агрегата определяют по инвариантам, которые должны сохраняться атомарно. Если статус заказа и его позиции нельзя считать корректными по отдельности, их следует включить в один агрегат с заказом в роли корня. Связанные данные, для которых допустима временная рассогласованность, должны находиться за пределами этого агрегата.
Понятие агрегата сформировалось в рамках предметно-ориентированного проектирования как способ управлять сложностью доменной модели. Исходная проблема состояла в том, что попытка изменять весь связанный граф объектов одной транзакцией ухудшала производительность, масштабируемость и понимание правил предметной области.
Агрегат вводит явную границу согласованности: внутри неё бизнес-инварианты поддерживаются синхронно, а внешние объекты взаимодействуют с агрегатом через его корень. Это позволяет отделить обязательную атомарность от обычной связанности данных.
Заказ может содержать статус, позиции, итоговую сумму и ограничения на изменение. При параллельных операциях один процесс способен изменить статус, пока другой меняет состав заказа. Если система не определила границу согласованности, итоговое состояние может нарушить бизнес-правило: например, оплаченный заказ окажется без обязательной позиции.
Ошибкой будет считать агрегатом всё, что связано с заказом в базе данных. Тогда изменение клиента, доставки или платежа может необоснованно блокировать заказ либо требовать общей распределённой транзакции. Обратная ошибка — разделить объекты, между которыми существует обязательный инвариант, и тем самым сделать корректное состояние недостижимым.
Сначала необходимо выписать инварианты предметной области. Например: оплаченный заказ нельзя изменить; итоговая сумма равна сумме его позиций; позиция не может существовать без заказа. Затем нужно определить, какие из этих правил должны проверяться в одной операции изменения.
Если правило требует атомарности заказа вместе с его позициями, заказ становится корнем агрегата, а позиции — внутренними сущностями. Внешний код не должен напрямую менять позицию в обход корня: он передаёт заказу команду, а агрегат проверяет инварианты и изменяет своё состояние целиком.
Остальные сущности не обязаны входить в тот же агрегат только из-за ссылки на заказ. Например, платёж может быть отдельным агрегатом: его состояние меняется по собственным правилам, а заказ получает результат через событие или отдельную команду. В таком случае между агрегатами возможна согласованность в конечном счёте, но бизнес-требования должны явно допускать короткое переходное состояние.
Для защиты от параллельных изменений обычно применяют версию агрегата или другой механизм обнаружения конфликта. Если версия изменилась после чтения, операция отклоняется или повторяется с актуальным состоянием; молчаливое затирание чужих изменений недопустимо.
Граница агрегата не обязана совпадать с таблицей или физической транзакцией базы данных. Она является прежде всего границей доменной согласованности, хотя часто влияет на структуру хранения и размер транзакции. Слишком крупный агрегат увеличивает блокировки, конфликты и стоимость чтения; слишком мелкий переносит сложные инварианты в ненадёжную координацию между сервисами.
В интернет-магазине заказ мог одновременно перейти в статус «оплачен» и получить удалённую позицию. Рассматривались три варианта. Первая стратегия — блокировать все связанные записи на время любой операции; она давала сильную согласованность, но ухудшала параллельность и усложняла интеграции. Вторая — сделать заказ, позиции и платёж одним большим агрегатом; это упростило бы атомарные проверки, но чрезмерно увеличило бы область блокировок.
Третья стратегия — оставить заказ вместе с позициями одним агрегатом, а платёж выделить отдельно. Изменение состава заказа проверяло версию заказа, а подтверждение платежа инициировало переход заказа через отдельную команду. Выбрали этот вариант, потому что правила заказа требовали немедленной согласованности с позициями, тогда как платёж имел самостоятельный жизненный цикл.
После внедрения конфликтующие изменения перестали затирать друг друга: одна операция получала отказ по устаревшей версии. Переход между состояниями заказа и платежа стал явно описанным процессом, а не побочным эффектом общей транзакции.
Нет. Связь, внешний ключ или совместное отображение на экран не доказывают необходимость общей атомарности. В один агрегат включают объекты, между которыми существуют инварианты, требующие проверки в рамках одной операции. Для остальных объектов достаточно ссылаться на идентификатор агрегата и взаимодействовать через публичные операции или события.
Таблицы описывают хранение, а агрегат — правила изменения доменного состояния. Одна таблица может содержать данные нескольких логических объектов, а один агрегат может храниться в нескольких таблицах. Если начать с таблиц, легко получить транзакции вокруг структуры данных вместо транзакций вокруг бизнес-инвариантов.
Нужно разделить обязательные инварианты и последовательность бизнес-шагов. Если правило действительно требует атомарного изменения, границы агрегатов, вероятно, выбраны неверно либо требуется специализированная координация. Если временная рассогласованность допустима, процесс можно реализовать через команды, события, повторные попытки и компенсацию, явно описав промежуточные состояния и последствия сбоя.