Когда я впервые столкнулся с DDD на проекте интернет-магазина, самая болезненная точка была не в терминологии и не в паттернах. Самое сложное — понять, где провести черту. Где заканчивается «Заказ» и начинается «Доставка»? Почему «Товар» в каталоге и «Товар» в корзине — это два разных понятия, хотя называют их одинаково?
Bounded Context — это не про технические границы. Это про смысл.
Bounded Context (ограниченный контекст) — явная граница, внутри которой определённая модель предметной области имеет однозначный смысл и не зависит от моделей в других контекстах.
Без таких границ вы получаете God-модель: одна сущность Product, которая пытается угодить каталогу, складу, ценообразованию и рекомендациям одновременно. Знакомо?
Почему без контекстов всё ломается
В типичном интернет-магазине слово «товар» означает разное в разных отделах. Для маркетинга важны описание, фото и SEO. Для склада — габариты, вес и ячейка хранения. Для бухгалтерии — себестоимость, НДС и поставщик.
Когда вы запихиваете всё это в одну модель, получается монстр на 50 полей. Половина из них null в зависимости от сценария. Разработчики боятся трогать Product.java, потому что непонятно, что отвалится.
Я видел проект, где сущность «Пользователь» содержала 83 поля. Там было всё: от хеша пароля до любимого цвета для рекомендаций. Каждый чих в профиле требовал регресса по всему приложению.
Bounded Context решает это радикально: у каждой части системы — своя модель. Свои поля, своя логика, свои инварианты. Одна и та же сущность в разных контекстах — это разные объекты.
Как выглядит разделение на контексты
Возьмём интернет-магазин и выделим ключевые контексты.
На диаграмме пять контекстов. Обратите внимание: «товар» в каждом — своя модель со своим набором полей. Контекст «Каталог» хранит название и описание, а контекст «Склад» — SKU и ячейку. Между собой контексты общаются событиями. ProductCreated сообщает складу и ценообразованию о новом товаре, OrderPlaced резервирует остатки.
Точечная линия от каталога к корзине — это не событие, а ссылка по идентификатору. Корзина не хранит описание товара, только productId. Если нужно название — запрашивает из каталога.
Как искать границы: три практических приёма
Хрестоматийные советы про «посмотрите на оргструктуру» и «ищите разный язык» — это верно, но недостаточно. На реальном проекте я использую три конкретных техники.
Приём 1: Тест на изменение
Задайте вопрос: «Если поменяется бизнес-правило X, какой код затронет?»
Если правило «цена товара зависит от сегмента клиента» живёт в той же модели, что и «товар имеет название и описание» — граница размыта. Изменение ценообразования не должно трогать каталог. Если трогает — режьте.
Практический тест: представьте, что один контекст отдаёте другой команде. Насколько сильно им придётся созваниваться? Если каждый спринт — это плохой разрез.
Приём 2: Анализ языка
Соберите с десяток пользовательских историй или требований и выпишите все существительные, которые используют стейкхолдеры. Одно и то же слово в разных отделах?
| Слово | Маркетинг | Склад | Бухгалтерия |
|---|---|---|---|
| Товар | Карточка с описанием и фото | Физическая единица на полке | Объект учёта с себестоимостью |
| Заказ | Конверсия, источник трафика | Задание на сборку | Финансовый документ |
| Клиент | Сегмент, LTV, канал | Адрес доставки, контакты | Плательщик, договор |
Три разных значения слова «Товар» — три разных контекста. Таблица сама показывает, где резать.
Приём 3: Правило одного источника правды
Каждый факт в системе должен иметь одного владельца. Цена товара меняется — кто source of truth? Если цена хранится и в каталоге, и в ценообразовании, и в заказе, вы обречены на рассинхрон.
В правильно нарезанных контекстах каждый атрибут принадлежит ровно одному контексту. Остальные либо ссылаются по ID, либо хранят копию на момент транзакции (как цена в заказе — это исторический снимок, не живая цена).
Типичные ошибки дробления
За годы практики я наступал на все эти грабли. Собрал самые частые.
Слишком мелко. Разрезать контекст до одного класса — перебор. Если между «контекстами» нет разных бизнес-правил, а только техническое разделение — это модули, не Bounded Context. Контекст — про бизнес-смысл, не про структуру кода.
Слишком крупно. God-контекст на всю «Клиентскую базу». Внутри уживаются профили, сегментация, история покупок и аутентификация. Результат: 40+ таблиц в одной схеме, команды блокируют друг друга, деплой раз в месяц со слезами.
Смешивание контекстов через общую БД. Видел проект, где «Заказы» и «Доставка» делили одну базу. Поле status означало разное в зависимости от контекста: в заказе — PAID, в доставке — IN_TRANSIT. Документация говорила «ориентируйтесь по соседним полям». Это не архитектура, это ребус.
События с избыточными данными. Контекст «Склад» получает событие ProductCreated и в него запихивают все 30 полей товара. Складу нужно два: productId и SKU. Остальное — информационный шум. Событие должно нести ровно то, что нужно подписчику для его бизнес-логики.
Отсутствие явных точек интеграции. Контексты живут сами по себе, но где-то всё равно происходит магия по перекладыванию данных. Обычно это скрыто в хранимых процедурах, триггерах или скриптах миграции. Точки интеграции должны быть явными: либо события, либо API, либо антикоррупционный слой.
Как Bounded Context связан с остальным DDD
Bounded Context не живёт в вакууме. Он — фундамент для остальных тактических паттернов DDD.
Агрегат и корень агрегата определяют границы транзакций внутри контекста. Каждый агрегат — это часть контекста, которая обеспечивает инварианты. Нельзя сделать агрегат, который ссылается на сущность из другого контекста (только по ID).
Ubiquitous Language развивается независимо в каждом контексте. Слово «Клиент» в контексте «Продажи» и в контексте «Поддержка» — это два разных ubiquitous language. Именно ради этого единства языка и нужна граница: внутри контекста все называют вещи одинаково.
Более широкий взгляд на DDD с примерами из банковской системы — в разборе DDD с банковским кейсом.
Контекстные карты: как показывать связи между контекстами
После того как контексты выделены, нужно показать, как они взаимодействуют. Для этого в DDD есть Context Map — карта контекстов с типами отношений.
Ключевые типы отношений на карте:
- Upstream/Downstream (U/D) — Каталог (upstream) диктует модель событий, Склад и Ценообразование (downstream) под неё подстраиваются. Upstream не знает о downstream, а downstream зависит от upstream.
- Partnership (PS) — Заказы и Доставка договариваются о контракте на равных. Обе стороны влияют на API.
- Conformist (CF) — Доставка полностью принимает модель данных Заказов без изменений. Нет смысла мапить поля, если команда доставки маленькая.
- Shared Kernel — два контекста делят общий кусок модели (например,
MoneyилиAddress). Режет только когда выгода перевешивает риск. - Anti-Corruption Layer (ACL) — адаптер между устаревшей системой и вашим контекстом, чтобы чужой хаос не просочился внутрь.
Чек-лист для аналитика
Перед тем как рисовать контексты на доске, пройдитесь по этому списку:
- Один и тот же термин в разных отделах означает разное? → Разные контексты.
- Есть бизнес-правило, которое меняет код в двух разных модулях одновременно? → Плохой разрез, режьте иначе.
- У каждого атрибута один владелец (source of truth)? → Проверьте, что
priceне живёт в трёх контекстах. - Контекст можно отдать другой команде без ежедневных созвонов? → Если нет, граница не работает.
- Точки интеграции между контекстами явные (события, API, ACL)? → Никаких триггеров в БД и хранимок-мостов.
- Bounded Context не меньше модуля и не больше поддомена? → Контекст — не класс и не вся система.
Этот чек-лист не гарантирует идеальный разрез с первого раза. DDD — итеративный процесс. Через три месяца работы вы пересмотрите границы, и это нормально. Архитектура — не каменная табличка, а живой организм.
Главное правило: если две части системы меняются по разным причинам и в разном темпе — они должны жить в разных Bounded Context. Всё остальное — детали реализации.