Главная

7 мин

Bounded context в DDD: как выделить домены и интеграции

Практичный разбор, как описать 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) как производитель и интегратор в Казахстане поставляет серверы и рабочие станции и берет на себя внедрение и поддержку инфраструктуры.

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

Частые вопросы

Зачем вообще нужен bounded context, если можно держать общую модель на всех?

Bounded context нужен, чтобы у терминов и правил было одно значение внутри конкретной зоны ответственности. Он снижает количество «случайных» поломок: вы меняете модель внутри контекста и не ломаете соседей, потому что наружу выходят только договоренные контракты.

С чего начать выделение доменов и границ, если система уже большая?

Начните с 5–10 ключевых процессов и ролей и выпишите термины так, как их реально используют в работе: в письмах, отчетах и тикетах. Там, где одни и те же слова означают разное или правила резко меняются, обычно и проходит граница контекста.

Как понять, что контекст получился слишком большим или слишком маленьким?

Хороший контекст можно объяснить одним предложением: какую цель он обслуживает и какие решения принимает. Если внутри постоянно спорят о смысле терминов, а изменения затрагивают слишком разные функции, граница слишком широкая; если любой простой сценарий требует дергать 3–4 модуля, граница слишком узкая.

Кто должен быть владельцем контекста и данных, и что это значит на практике?

Владелец отвечает не за таблицу, а за смысл: определения терминов, правила изменений и качество данных. Без владельца справочники и статусы начинают «править по просьбе», и вы быстро теряете совместимость между командами.

Как договориться, кто «владеет» данными, чтобы модули не спорили и не переписывали друг друга?

Обычно выбирают один «источник правды» для каждого ключевого понятия: кто создает и меняет, остальные читают и держат копии ровно того, что им нужно. Если всем разрешить править одну сущность, вы получите скрытые зависимости и конфликты при релизах.

Когда выбирать API, а когда события между контекстами?

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

Как сделать контракт интеграции таким, чтобы его можно было менять без боли?

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

Что такое антикоррупционный слой (ACL) и когда он действительно нужен?

ACL ставят на стороне потребителя, чтобы принимать внешние сообщения и переводить их в свои термины и статусы. Это защищает модель: вы не импортируете чужие справочники и правила напрямую, а явно маппите значения и отсекаете некорректные случаи.

Когда стоит вводить сагу для сквозного процесса, а когда достаточно событий?

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

Можно ли применять bounded context в монолите, или это только для микросервисов?

Да, bounded context работает и в монолите: границы можно оформить на уровне модулей и контрактов в коде, чтобы команды не смешивали модели. Часто это лучший старт, а инфраструктуру и интеграции (очереди, наблюдаемость, сервера, поддержку 24/7) подключают по мере зрелости пилота и роста нагрузки.

Запрос

Пришлите спецификацию. Остальное сделаем мы

Одна позиция или объект под ключ, любое направление. Вашу поставку от первого звонка до акта ввода ведёт один менеджер.

Расчёт бесплатноДокументы для госзакупок и тендеровОдин договор и одна гарантия