Прямой ответ
Ограниченный контекст — это область, в пределах которой модель предметной области непротиворечива. Одно и то же слово в разных контекстах означает разные вещи, и попытка построить единую модель для всей системы приводит к объекту с тридцатью полями, из которых каждому сценарию нужны пять.
Классический пример — «товар»:
| Контекст | Что такое товар |
|---|---|
| Каталог | Название, описание, фотографии, категория |
| Склад | Ячейка хранения, остаток, габариты, вес |
| Доставка | Объём, вес, ограничения по перевозке |
| Ценообразование | Себестоимость, наценка, акции |
Это четыре разные модели, связанные общим идентификатором. Попытка объединить их в один класс создаёт сущность, которую невозможно изменить без согласования с четырьмя командами.
Второе ключевое понятие — единый язык. Термины, которыми оперирует бизнес, должны совпадать с именами в коде. Если менеджер говорит «резерв», а в коде это hold_flag, расхождение накапливается в каждом обсуждении и в каждой ошибке.
Третье — агрегат. Это группа объектов, изменяемая как единое целое в одной транзакции:
class Order: # корень агрегата
def __init__(self, order_id: str, customer_id: str):
self.id = order_id
self.customer_id = customer_id
self._items: list[OrderItem] = []
self.status = OrderStatus.NEW
def add_item(self, sku: str, qty: int, price: Decimal) -> None:
if self.status is not OrderStatus.NEW:
raise DomainError("Нельзя менять состав подтверждённого заказа")
self._items.append(OrderItem(sku, qty, price))
def total(self) -> Decimal:
return sum((i.price * i.qty for i in self._items), Decimal(0))
Правила агрегата:
- Один корень. Внешний код обращается только к нему; вложенные объекты недоступны напрямую.
- Ссылки по идентификатору. Агрегат ссылается на другой агрегат через идентификатор, а не через объект.
- Одна транзакция — один агрегат. Изменение двух агрегатов в одной транзакции означает, что граница проведена неверно.
- Инварианты внутри. Проверка бизнес-правил выполняется в корне агрегата, а не в вызывающем коде.
Практика: применение без академизма
1. Начинайте со стратегической части, а не с тактической. Границы контекстов важнее шаблонов кода. Неправильная граница обходится дороже, чем отсутствие value object.
2. Ищите границы по языку, а не по таблицам. Если одно слово в разных отделах означает разное — это разные контексты. Если бизнес-процесс не пересекает отдел — вероятно, это один контекст.
3. Не делайте агрегат большим. Крупный агрегат блокируется целиком и конфликтует при параллельной работе. Практический признак: если два пользователя регулярно мешают друг другу, граница слишком широкая.
4. Согласованность между агрегатами — через события. Требование «остаток на складе обновился сразу после заказа» почти всегда на самом деле означает «обновился в течение секунды». Явная согласованность нужна только внутри агрегата; о моделях согласованности — в статье про eventual consistency.
5. Контекст не равен сервису. Несколько контекстов могут жить в одном приложении; о границах модулей — в статье про модульный монолит. Обратное тоже верно: сервис может реализовывать часть контекста.
6. Фиксируйте единый язык в словаре. Расхождение терминов — источник ошибок в требованиях. Словарь терминов дешевле, чем разбор неверной реализации.
7. Не переносите чужую модель в свой контекст. Если внешняя система оперирует своими понятиями, их нужно транслировать, а не копировать внутрь; о приёме — в статье про антикоррупционный слой.
Рекомендации:
- Рисуйте карту контекстов до написания кода. Схема из десятка прямоугольников со связями выявляет проблемы быстрее, чем реализация.
- Проверяйте границы по частоте изменений. Если два модуля почти всегда меняются вместе, это один контекст, разделённый искусственно.
- Избегайте общей модели данных. Общая база на несколько контекстов делает их изменение согласованным — см. статью про общую базу как антипаттерн.
- Используйте идентификаторы для связей. Ссылка по идентификатору переживает изменение чужой модели; ссылка по объекту — нет.
- Не внедряйте DDD целиком. Агрегаты и контексты полезны почти всегда; полный набор тактических шаблонов оправдан только в сложной предметной области.
Практический сценарий: единая модель заказа тормозила все изменения
Контекст: в системе был один класс Order с 42 полями, который использовали каталог, доставка, бухгалтерия и поддержка. Любое изменение требовало согласования с четырьмя командами.
- Диагностика. Анализ коммитов показал, что 70 % изменений класса приходились на два сценария, а остальные поля использовались в отдельных местах. Число полей росло, поскольку каждый сценарий добавлял своё.
- Разделение по языку. Обсуждение с командами выявило, что «заказ» в доставке — это отправление с адресом и габаритами, в бухгалтерии — документ с суммами и ставками, в поддержке — обращение с историей действий.
- Действия. (а) Выделили три контекста с собственными моделями, связанными идентификатором заказа; (б) в каждом контексте оставили только нужные поля; (в) обмен организовали через события: создание заказа публиковало событие, остальные контексты строили собственное представление; (г) ввели словарь терминов, поскольку слово «позиция» в трёх контекстах означало разное; (д) инварианты перенесли в корень агрегата вместо разбросанных проверок.
- Результат. Средний размер модели сократился с 42 до 9–12 полей; изменение в доставке перестало затрагивать бухгалтерию, а число конфликтов слияния в общем файле исчезло.
Вывод: рост числа полей в общей модели — признак отсутствия границ, а не недостатка рефакторинга. Разделение по смыслу, который вкладывают в термин разные участники, даёт устойчивые границы.
FAQ
Обязателен ли отдельный сервис на каждый контекст? Нет. Контекст — это граница модели, а сервис — граница развёртывания. Несколько контекстов могут жить в модульном монолите.
Что делать, если бизнес настаивает на единой модели? Показать конкретные случаи, когда изменение в одной области ломало другую. Аргумент работает лучше теоретических рассуждений.
Как определить размер агрегата? По инварианту: всё, что должно оставаться согласованным в одной транзакции, входит в агрегат. Всё остальное — за его пределами.
Нужны ли value object и репозитории в простом CRUD? Не обязательно. Тактические шаблоны оправданы там, где есть нетривиальные бизнес-правила; для простого хранилища достаточно обычной структуры.
Источники: Martin Fowler (BoundedContext, DDD_Aggregate), domainlanguage.com — ссылки в шапке.