Bounded context в DDD: как выделить домены и интеграции
Практичный разбор, как описать bounded context в DDD, задать границы модулей и правила интеграций, чтобы масштабирование не ломало разработку.

Зачем вообще делить домены и вводить границы
Почти любая корпоративная система начинается как «один монолит с модулями». Сначала это удобно: один репозиторий, одна база, общие справочники. Но по мере роста функционала модули начинают спорить за одни и те же сущности и правила. Изменение в одном месте неожиданно ломает другое.
Первые симптомы обычно одинаковые: несколько команд правят одни и те же таблицы «на всякий случай»; общий словарь терминов расползается (один и тот же «клиент» или «заказ» означает разное); в общие очереди и топики все пишут что хотят, а потом все вынуждены разбирать чужие форматы; «общие» утилиты тянут за собой полсистемы. Релизы становятся редкими, потому что тестировать приходится все сразу.
Когда команд становится больше, цена согласований растет быстрее, чем сама разработка. Архитектура превращается в переговорный клуб: чтобы добавить одну функцию, нужно договориться с несколькими владельцами и пересмотреть десяток зависимостей.
Здесь и помогает bounded context в DDD: явные границы, внутри которых модель и термины согласованы, а наружу выходят только договоренные интерфейсы. Граница нужна не «для красоты», а чтобы изменения были локальными и предсказуемыми.
Ключевые понятия DDD без лишней теории
DDD - это способ договориться о смыслах. Команда описывает систему так, как о ней говорят люди из бизнеса, и превращает этот язык в модель: сущности, правила, статусы, сценарии. Когда модель и разговорный язык совпадают, меньше ошибок в требованиях и меньше «переводов» между аналитиками, разработчиками и пользователями.
Домен - это область бизнеса, ради которой существует система (например, закупки, учет, техподдержка). Внутри домена обычно есть поддомены: части разной важности и сложности. Где-то это «ядро» (то, что дает конкурентное преимущество), а где-то - типовая рутина, которую можно делать проще.
Модель - это не диаграмма и не база данных. Это набор понятий и правил: что считается «заявкой», когда она «одобрена», кто может менять сумму, какие статусы допустимы. Важно, что модель живет в коде и в речи команды.
Контекст - это место, где слова имеют строгое значение и где действует одна модель. Граница контекста обычно проходит по ответственности: кто владеет правилами, кто отвечает за данные, кто принимает решения об изменениях.
Одна и та же фраза может означать разное в разных местах. «Клиент» для отдела продаж - это компания, для бухгалтерии - плательщик, для поддержки - пользователь с инцидентами. Попытка сделать одно определение на всех приводит к тому, что модель расползается, а изменения становятся болезненными.
DDD особенно полезен в корпоративных системах, где много команд и интеграций: модули развиваются параллельно, требования часто меняются, много согласований и исключений, термины используются по-разному, а интеграция идет через несколько систем (ERP, CRM, сервис-деск).
В проектах системной интеграции такие договоренности о смыслах часто важнее, чем выбор фреймворка: они защищают систему от хаоса при росте.
Что такое bounded context и как понять его границы
Bounded context в DDD - это четкая зона ответственности в системе, где слова и правила означают одно и то же для всех участников. Внутри такой зоны модель не спорит сама с собой: если вы говорите «Заказ», команда понимает, что это именно здесь и какие у него состояния.
Хороший контекст обычно узнается по трем вещам: у него есть понятная цель, понятные владельцы и устойчивый язык (термины не меняют смысл от экрана к экрану).
Внутри границы должны жить ключевые сущности, бизнес-правила и данные, которые нужны этим правилам, а также интерфейсы, через которые контекст обслуживает других (API, команды, сценарии). На границе лучше держать договоренности об интеграции: контракты, события, форматы и правила совместимости. Тогда изменения внутри реже ломают соседей.
Проверка на ширину границы простая.
- Слишком широкая граница: много конфликтующих терминов, частые споры «как правильно», слишком разные цели в одном месте.
- Слишком узкая граница: каждое действие требует похода в 3-4 модуля, много лишних интеграций ради простых операций.
Пример: в закупках «Поставщик» - это контрагент с договорами и условиями поставки, а в бухгалтерии «Поставщик» - запись для платежей и отчетности. Если пытаться сделать одного «Поставщика» на всех, модель быстро превращается в компромисс и начинает мешать.
Чтобы зафиксировать ответственность, заранее назначьте владельца контекста: кто утверждает термины, правила и изменения модели, и кто принимает решения при конфликте с соседними командами.
Как выделять домены: практичный подход для корпораций
Начните не с модулей и не с оргструктуры, а с бизнес-целей. Какие решения система должна помогать принимать каждый день: что можно утвердить, что нельзя, где нужны согласования, где важна скорость, а где важны точность и аудит.
Дальше соберите 5-10 ключевых процессов и ролей, но не уходите в экраны и кнопки. Полезнее описать поток работ простыми словами: кто инициирует действие, кто проверяет, кто несет ответственность, какой результат считается правильным.
Почти всегда границы появляются там, где меняются правила и словарь. Если одни и те же слова означают разное для разных команд, это сигнал, что вы уже находитесь в разных bounded context в DDD. Например, «заказ» в закупках может быть заявкой на покупку, а в логистике - конкретной поставкой с датами и статусами.
Чтобы домены не спорили о данных, заранее отметьте источники правды. В корпорациях часто один контекст владеет справочником контрагентов, другой - ценами, третий - остатками. Важно не пытаться сделать один общий «идеальный объект», а договориться, кто создает, кто читает, и что считается официальным.
Для первичного разбиения помогает простая шкала:
- Core: то, что дает компании отличие и где чаще всего меняются правила.
- Supporting: важные вещи, но без уникальности, их можно делать проще.
- Generic: типовые функции вроде авторизации, уведомлений, отчетных выгрузок.
Пример: в интеграторском проекте для крупного заказчика core может быть управление контрактами и закупочными лимитами (много регуляторики и согласований), supporting - учет поставок и гарантий, generic - каталог пользователей и роли. Такой взгляд помогает понять, где держать сильную доменную модель, а где не усложнять.
Пошагово: как оформить карту контекстов и договоренности
Карта контекстов нужна, чтобы все одинаково понимали, где заканчивается одна модель и начинается другая. Начинайте не с инструментов и «идеальных схем», а с языка и ответственности.
Соберите термины из живых источников: экранов, отчетов, писем, договоров, тикетов. Рядом фиксируйте, где слово используется и что именно под ним имеют в виду. Один и тот же «клиент» в продажах и «клиент» в поддержке - это часто разные понятия, и лучше увидеть это сразу.
Дальше определите владельца данных для каждого ключевого термина. Владелец не «хранит таблицу», а отвечает за смысл, правила изменений и качество. Например, «Статус заказа» должен иметь одного хозяина, даже если его читают пять модулей.
После этого набросайте контексты на одном листе: 5-8 прямоугольников, у каждого короткая цель (одна фраза) и границы. Тут важнее не идеальное разбиение, а возможность в любой момент сказать: «это не наше» и «вот где начинается наше».
Теперь опишите входы и выходы каждого контекста: команды, запросы, события, файлы. Чтобы договоренности не расплылись, зафиксируйте минимум:
- глоссарий терминов и значения по контекстам;
- владельцев данных и правила изменений;
- интерфейсы (что отдаем и что ожидаем);
- способ интеграции (API, события, выгрузки);
- версии контрактов и правила совместимости.
Финальный шаг - согласование границ с командами и планом релизов. Проверьте, кто поддерживает контракт, как выкатывать изменения без остановки соседей, и что делать при конфликте значений (например, через маппинг или отдельный слой защиты модели).
Правила интеграций между модулями: что важно решить заранее
Интеграции между модулями чаще ломают масштабирование не из-за технологий, а из-за неясных ожиданий: кто кому что должен, когда, в каком формате и что делать при сбоях. Границы помогают, но без правил обмена данные и ответственность начинают «протекать» между командами.
Интеграция по API подходит, когда нужен быстрый ответ здесь и сейчас: проверить лимит, получить статус, рассчитать цену по актуальным правилам. Важно заранее договориться, какие операции критичны, какие таймауты приемлемы, и что считается «нормальной» ошибкой (например, нет данных vs сервис недоступен).
Событийная модель лучше, когда изменения должны разойтись по системе без жестких зависимостей: «Заявка согласована», «Счет оплачен», «Товар принят на склад». Это снижает связность, но требует дисциплины: событие должно быть понятным, стабильным и не превращаться в «передачу всей внутренней модели».
Пакетные обмены уместны там, где нет смысла гонять онлайн-запросы: ночная сверка, загрузка справочников, выгрузка отчетов в сторонний контур. Минус простой: данные всегда немного устаревшие, а ошибки обнаруживаются поздно.
Для выбора синхронного или асинхронного обмена обычно хватает критериев: нужен ответ пользователю прямо сейчас - API; можно обработать позже - события; допустима задержка часами - пакет; сбой внешнего модуля не должен блокировать ваш - асинхронно; важнее единая точка правды, чем скорость - чаще синхронно.
Отдельная тема - контракт. Есть вещи, которые потом особенно больно менять: версионирование (как долго живут старые версии), обратная совместимость, правила ошибок (коды, тексты, повторная попытка, дедупликация), схема данных (обязательные поля, форматы дат и сумм), ответственность за изменения (кто уведомляет и как тестируется).
Пример: модуль «Закупки» отправляет событие «Заказ подтвержден», а модуль «Учет» не тянет детали через прямой вызов, а хранит только то, что ему нужно для проводок. Тогда изменения в «Закупках» реже ломают «Учет», даже когда команды растут.
Как не смешивать модели: контракты, маппинг и антикоррупционный слой
Когда два bounded context в DDD начинают напрямую обмениваться своими сущностями, модель быстро «заражается» чужими правилами. В итоге каждый релиз ломает соседей, а команды спорят о смысле полей. Спасает простой принцип: интеграция почти всегда требует перевода, даже если данные похожи.
Перевод нужен потому, что одно и то же слово часто значит разное. В продажах «клиент» может быть человеком, а в биллинге «клиент» - плательщик с договором и лимитами. Если тянуть один объект в другой модуль, вы незаметно переносите и его ограничения.
Антикоррупционный слой (ACL)
Антикоррупционный слой размещают на стороне потребителя, то есть в том контексте, который получает данные. Он принимает внешние сообщения, проверяет их, переводит в свои термины и только потом сохраняет.
Обычно в ACL делают маппинг полей и форматов (коды, даты, валюты), проверку смысла (например, не принимать «закрытую» заявку как «готовую к оплате», если правила другие), изоляцию справочников (свои статусы и типы, а не чужие) и обработку несовпадений (значение неизвестно, поле отсутствует, правило конфликтует).
Контракты данных: меньше, но точнее
Контракт лучше держать минимальным и понятным: только то, что реально нужно получателю, плюс четкие значения. Не «передаем всю заявку», а «передаем факт и ключевые атрибуты». Полезно явно договориться, какие поля стабильны, какие могут быть пустыми и что считается источником истины.
Хорошая практика - не импортировать чужие статусы и справочники. Принимайте внешний статус как сигнал и переводите в свой. Например, внешний APPROVED у вас может стать «Разрешено к заказу», а иногда - «Требует проверки», если не совпали лимиты или роли.
Если термины расходятся, решайте это в контракте и маппинге, а не через «единый справочник для всех». Так границы остаются чистыми, а развитие не превращается в бесконечные переделки.
События, процессы и согласованность данных между контекстами
События между bounded context помогают командам не тянуть чужие таблицы и не зависеть от внутренней логики соседнего модуля. Но не каждое изменение стоит превращать в событие.
Публикуйте наружу только то, что важно другим контекстам и понятно бизнесу: «Заказ подтвержден», «Счет выставлен», «Оборудование отгружено». А вот «Пользователь нажал кнопку» или «Статус изменился с 3 на 4» лучше оставлять внутренним: это детали интерфейса и реализации, они часто меняются и ломают интеграции.
Когда один бизнес-процесс проходит через несколько контекстов, появляется вопрос: кто им управляет. Если шаги можно выполнять независимо, обычно хватает реакций на события. Если нужен строгий порядок, таймауты, откаты или ручные решения, без саги часто не обойтись. Сага - это «дирижер процесса», который следит, что шаг 1 завершился, затем запускает шаг 2, а при проблеме делает компенсацию (например, отменяет резерв).
Идемпотентность нужна потому, что события могут прийти дважды: из-за повторной доставки, ретраев или сбоя сети. Получатель должен уметь безопасно обработать повтор. Практически это означает: у события есть стабильный идентификатор, а обработчик хранит факт обработки и не создает дубликаты (не заводит второй счет, не списывает деньги повторно).
Конечную согласованность лучше проговорить с бизнесом и QA заранее. После события данные в другом контексте обновятся не мгновенно, а через секунды или минуты. Это нормально, если есть правила: где показываем «в обработке», какие действия запрещаем до подтверждения, какие задержки считаем приемлемыми.
Чтобы события не превратились в слухи и догадки, документируйте каждое публичное событие как контракт: имя и бизнес-смысл, когда публикуется и кто источник, поля и их значения (обязательные и опциональные), ожидания по доставке и порядку (хотя бы на словах: возможен дубль, возможна задержка), правила версионирования (как добавляем поле, не ломая потребителей).
Частые ошибки, из-за которых масштабирование ломает разработку
Проблемы обычно начинаются не на этапе рисования границ, а когда команда растет и каждый модуль начинает менять данные и смыслы по-своему. Даже если вы договорились про bounded context в DDD, ошибки во владении моделью и интеграциях быстро превращают изменения в цепочку багов.
Ошибка 1: границы рисуют по таблицам, а не по ответственности
Частый сценарий: модуль называют отдельным, потому что у него своя схема в базе. Но ответственность остается размытой: кто решает, что такое «статус» или «клиент»? В итоге доменная логика расползается по сервисам и триггерам.
Ошибка 2: одна модель на всех, потому что так быстрее
Единая «каноническая» сущность кажется удобной: меньше маппинга, меньше кода. Но позже любой термин начинает означать разные вещи для разных команд. «Заказ» в закупках и «заказ» в бухгалтерии живут по разным правилам, и единая модель превращается в компромисс, который мешает всем.
Еще несколько типичных поломок:
- общие справочники без владельца (правки идут «по просьбе», без правил и совместимости);
- сквозные транзакции через несколько контекстов (любая задержка блокирует всех);
- интеграция через прямой доступ к базе или внутренним методам (зависимость от деталей реализации);
- отсутствие версионирования контрактов (одно поле рушит потребителей);
- смешение «данных» и «смысла» (переносите чужие поля, не понимая интерпретацию).
Мини-пример: команда закупок добавила новый статус «Частично поставлено», а команда учета использует статус как триггер проводок. Если контракт не версионирован и нет явного маппинга (или ACL), учет начнет проводить лишнее или не проведет вовсе.
Хороший признак зрелости: у каждого справочника и события есть владелец, контракты меняются по правилам, а интеграции идут через публичные границы, а не через «дайте доступ, так быстрее».
Короткий чеклист перед разделением и ростом команды
Перед тем как отделять модули и раздавать их разным командам, стоит пройти короткую проверку. Она помогает убедиться, что границы не нарисованы на глаз, а интеграции не превратятся в хаос через пару релизов.
Начните с главного: у каждого bounded context в DDD должна быть понятная цель и владелец. Если нельзя одним предложением объяснить, за что отвечает контекст (и кто принимает решения по его модели), значит граница пока не созрела.
Проверьте базовые вещи:
- границы данных: что хранится здесь, а что только читается снаружи;
- список интеграций и их тип (API, события, пакетная выгрузка);
- версионирование контрактов и правило совместимости;
- политика по справочникам: один источник правды или копии, и кто отвечает за качество;
- для сквозных процессов - кто оркестрирует шаги (процесс в одном контексте или отдельный процессный модуль).
Дальше быстро проверьте риски смешивания моделей. Если модуль А напрямую пишет в таблицы модуля Б или «подглядывает» в его внутренние поля, отделение будет болезненным. Договоритесь заранее: только через явные контракты и понятный маппинг.
Критерий готовности простой: контекст можно выпускать и менять независимо. Например, команда учета обновляет правила проводок без срочного патча от команды закупок из-за изменения внутреннего поля «Статус». Если так не получается, вернитесь к границам и интеграциям и уточните их до масштабирования.
Пример: как разделить систему закупок и учета без боли
Представьте корпорацию, где есть закупки, склад, финансы и ITSM. Команда хочет ускорить изменения, но сейчас все сидит в одной базе, а любой новый отчет ломает полсистемы. Здесь bounded context в DDD помогает разложить ответственность так, чтобы рост команды не превращался в вечные согласования.
Стартовая нарезка по контекстам часто выглядит так:
- Заказы: заявки, согласования, выбор поставщика, статус жизненного цикла заказа.
- Поставки: отгрузка, приемка, расхождения, партии, документы.
- Бюджеты: лимиты, статьи, резервирование, факт, контроль.
- Активы: единицы учета, инвентарные номера, перемещения, списания.
Боль обычно начинается там, где слова одинаковые, а смысл разный. «Статус заказа» в Закупках - это «на согласовании» или «выбран поставщик», а в Поставках статус больше про логистику и приемку. «Единица учета» в Активах - инвентарная позиция, а на складе - штука, коробка или партия. «Приемка» для Поставок - подтверждение количества и качества, а для Финансов - основание признать обязательство и провести платеж.
Интеграции лучше заранее разделить на события и запросы. Например, Поставки публикуют событие «Товар принят» (номер заказа, позиции, количество, даты), а Бюджеты и Финансы подписываются и обновляют свои состояния. При этом Закупки не ходят прямыми запросами в Бюджеты за каждой кнопкой. Вместо этого есть простой API: «проверить лимит» и «зарезервировать сумму» с четким контрактом.
Чтобы не вернуться к монолиту, держите правило: никаких общих таблиц и прямых SQL между модулями. Каждый контекст владеет своей базой и своей моделью, а обмен на границах идет через контракты (события и API) с явным маппингом полей и смыслов.
Следующие шаги: пилот, инфраструктура и поддержка изменений
Начните с пилота, а не с попытки «разделить все сразу». Обычно достаточно 1-2 контекстов, где уже болит: частые конфликты между командами, сложные релизы, много ручных согласований. Хорошие кандидаты - контекст с заметной ценностью для бизнеса и минимальным числом внешних зависимостей.
Чтобы пилот не превратился в спор о терминах, заранее договоритесь, как поймете, что стало лучше. Достаточно 3-4 метрик и просмотра на ретро раз в 2-4 недели: время от запроса до релиза (lead time) для изменений в пилотных модулях, количество конфликтов требований и «перекидываний» задач, число инцидентов после релиза и время восстановления, количество экстренных правок «в обход правил» (ручные синхронизации, прямые запросы в чужую БД).
Дальше подготовьте минимальный набор инфраструктуры и правил, чтобы границы не расползались: единый способ описывать контракты, отдельные окружения, мониторинг. На старте обычно хватает документации по API/событиям с владельцами договоренностей, тестовых окружений для интеграций и проверки совместимости версий, логов/метрик/алертов по ключевым потокам, резервного копирования и плана восстановления для критичных данных.
Когда пилот показывает результат, часто упираются в «железную» и сервисную базу: серверы под сервисы и очереди, хранилища, рабочие места для команд, поддержка 24/7. В таких задачах полезен системный интегратор, который закрывает цепочку целиком. Например, GSE.kz (gse.kz) как производитель и интегратор в Казахстане поставляет серверы и рабочие станции и берет на себя внедрение и поддержку инфраструктуры.
Главное правило на этом этапе: расширяйте карту контекстов только после того, как пилотные границы выдержали несколько релизов и инцидентов без отката к «общей модели для всех».
FAQ
Зачем вообще нужен bounded context, если можно держать общую модель на всех?
Bounded context нужен, чтобы у терминов и правил было одно значение внутри конкретной зоны ответственности. Он снижает количество «случайных» поломок: вы меняете модель внутри контекста и не ломаете соседей, потому что наружу выходят только договоренные контракты.
С чего начать выделение доменов и границ, если система уже большая?
Начните с 5–10 ключевых процессов и ролей и выпишите термины так, как их реально используют в работе: в письмах, отчетах и тикетах. Там, где одни и те же слова означают разное или правила резко меняются, обычно и проходит граница контекста.
Как понять, что контекст получился слишком большим или слишком маленьким?
Хороший контекст можно объяснить одним предложением: какую цель он обслуживает и какие решения принимает. Если внутри постоянно спорят о смысле терминов, а изменения затрагивают слишком разные функции, граница слишком широкая; если любой простой сценарий требует дергать 3–4 модуля, граница слишком узкая.
Кто должен быть владельцем контекста и данных, и что это значит на практике?
Владелец отвечает не за таблицу, а за смысл: определения терминов, правила изменений и качество данных. Без владельца справочники и статусы начинают «править по просьбе», и вы быстро теряете совместимость между командами.
Как договориться, кто «владеет» данными, чтобы модули не спорили и не переписывали друг друга?
Обычно выбирают один «источник правды» для каждого ключевого понятия: кто создает и меняет, остальные читают и держат копии ровно того, что им нужно. Если всем разрешить править одну сущность, вы получите скрытые зависимости и конфликты при релизах.
Когда выбирать API, а когда события между контекстами?
API лучше, когда нужен ответ прямо сейчас и действие зависит от текущего состояния, например проверка лимита или расчет цены. События лучше, когда важно разнести факт изменения по системе без жесткой связности, и допустима задержка в обновлении данных у получателей.
Как сделать контракт интеграции таким, чтобы его можно было менять без боли?
Держите контракт минимальным: передавайте факты и нужные атрибуты, а не всю внутреннюю модель. В контракте заранее фиксируйте обязательные поля, допустимые значения, правила ошибок и версионирование, чтобы добавление нового поля не ломало потребителей.
Что такое антикоррупционный слой (ACL) и когда он действительно нужен?
ACL ставят на стороне потребителя, чтобы принимать внешние сообщения и переводить их в свои термины и статусы. Это защищает модель: вы не импортируете чужие справочники и правила напрямую, а явно маппите значения и отсекаете некорректные случаи.
Когда стоит вводить сагу для сквозного процесса, а когда достаточно событий?
Сага нужна, когда процесс проходит через несколько контекстов и важен порядок шагов, таймауты и компенсации при сбоях. Если шаги независимы, часто хватает реакции на события; но если без строгой последовательности бизнес-результат будет неверным, оркестратор процесса оправдан.
Можно ли применять bounded context в монолите, или это только для микросервисов?
Да, bounded context работает и в монолите: границы можно оформить на уровне модулей и контрактов в коде, чтобы команды не смешивали модели. Часто это лучший старт, а инфраструктуру и интеграции (очереди, наблюдаемость, сервера, поддержку 24/7) подключают по мере зрелости пилота и роста нагрузки.