lenec ru

← все посты

Bounded context: как разрезать предметную область, чтобы команды не толкались локтями

17K

Когда смотришь, как зрелая команда разрезает большую систему, разговор почти всегда сводится не к таблицам и не к сервисам, а к тому, какими словами разные группы людей называют одно и то же. У саппорта «клиент» — это аккаунт с историей обращений. У биллинга «клиент» — плательщик с реквизитами. У маркетинга «клиент» — это посетитель, у которого ещё может не быть аккаунта. Если все эти «клиенты» сидят в одной модели и одной таблице, ты получаешь ту самую большую сложную сущность с тридцатью полями, в которой каждое изменение ломает что-нибудь у соседей.

Bounded context (ограниченный контекст) из DDD — это не модный термин, а способ нарезать систему по реальным границам понятий. В одном контексте «клиент» означает одно, в другом — другое, и эти модели спокойно живут рядом, не пытаясь договориться об общем определении. Расскажу, как это выглядит, когда работает, и какие сигналы говорят, что границы провели не там.

Зачем вообще границы

Главная проблема большой системы — не в технологиях, а в людях. Когда десять разработчиков и три аналитика пытаются согласовать модель «клиента» одновременно с двадцатью пользовательскими сценариями, согласование становится бесконечным. Любое поле, добавленное для маркетинга, обсуждается с биллингом, потому что «вдруг сломаем».

Bounded context разделяет эту работу. Внутри границы команда говорит на одном языке (его называют ubiquitous language), и любое изменение — её собственная зона ответственности. На границе с соседом действует контракт: то, что мы друг другу обещаем. Этот контракт меняется только согласованно. Внутри — нет.

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

Где провести границу

Самый частый и плохой совет — «по сущностям» или «по микросервисам». Граница bounded context живёт не там. Она там, где у одного и того же слова разные значения, или где набор операций над сущностью разный.

Пример. В системе есть «продукт». Для каталога продукт — это карточка с описанием, ценой, картинками, SEO-полями. Для склада продукт — это SKU, количество на полке, поставщики. Для аналитики продукт — это товар с историей продаж и сегментами клиентов. Это три разных контекста, и попытка сделать одну сущность Product с пятью десятками полей закончится тем, что каталог тормозит из-за join-ов на склад, а аналитика молится, чтобы изменения в каталоге не сломали отчёты.

Хороший признак границы — переход через неё требует перевода. Если из контекста «склад» в контекст «каталог» поле передаётся как есть, без интерпретации, то либо граница не нужна, либо там утечка модели. Если требуется явный маппинг — граница на месте.

Стратегические паттерны: как контексты соотносятся

В DDD есть классификация отношений между контекстами. Пройдусь по тем, которые встречаю в проде чаще всего.

Customer/Supplier

У одного контекста есть «клиент» (потребитель данных) и «поставщик». Поставщик публикует API, клиент его потребляет. Поставщик прислушивается к запросам клиента, но владеет своим контрактом. Это самый здоровый случай: видна власть и видна ответственность.

Conformist

Клиент вынужден принимать модель поставщика как есть, без права на возражения. Обычно это интеграция с внешней системой или с другой компанией. Признак — у вас есть классы с именами как у соседа: SapMaterial, HrEmployee. Часто за этим следует жалоба «нашу модель портит чужая». В этом случае защищайте свой контекст через anti-corruption layer.

Anti-corruption layer (ACL)

Слой перевода чужой модели в свою. Внутри контекста живёт ваш язык, на границе стоит переводчик. Это не отдельный паттерн отношений, а защитное сооружение. Полезно, когда сосед — легаси или внешний партнёр, и его модель не должна расползаться по вашему коду.

Shared kernel

Два контекста делят небольшое ядро общей модели. Это привлекательно и опасно: всё, что в shared kernel, требует согласования между двумя командами. Я применяю shared kernel максимум для очень стабильных вещей: общие value objects, идентификаторы, базовые перечисления. Любое поведение туда тянуть нельзя.

Open Host Service + Published Language

Контекст публикует понятный наружу контракт (REST API, события в Kafka со схемой) и явный язык, в котором этот контракт описан. Это разумная цель для системы, к которой подключаются больше двух потребителей.

Контекстная карта

Без визуального представления, какие контексты есть и как они связаны, всё это превращается в разговоры. Я обычно рисую контекстную карту: квадраты — контексты, стрелки — отношения, на стрелках — тип (customer/supplier, conformist и т.д.).

Карта не должна быть красивой. Она должна быть актуальной. Я делаю её в Miro или просто в текстовом виде, и обновляю каждые пару кварталов. Главная её ценность не в первичной отрисовке, а в том, что когда возникает вопрос «а с кем мы должны согласовать это изменение», ответ виден глазами, а не по памяти.

Контекст и сервис: не одно и то же

Частая путаница: «один bounded context = один микросервис». Это не правило, а упрощение. На практике контекст может быть реализован как:

  • Один сервис. Самый частый случай в зрелых архитектурах.
  • Модуль внутри монолита. На ранних этапах системы — норма.
  • Несколько сервисов, делящих одну доменную модель (например, командный сервис и read-модель в CQRS). Тоже один контекст.

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

Сигналы, что границы не там

  • Большие транзакции через несколько контекстов. Если оформление заказа в транзакции трогает таблицы каталога и биллинга, у вас, скорее всего, не три контекста, а один с зачем-то разделёнными границами.
  • Постоянные совещания на стыке. Если каждое второе изменение требует встречи двух команд — границы провели не там, или контекст вышел за свои рамки и пытается принимать решения за соседа.
  • Чужие сущности в коде. Когда внутри контекста billing появляется класс Customer с полями из CRM — границы протекли, и пора ставить ACL.
  • Один и тот же термин с разными значениями без перевода. Если в коде свободно ходит «product», и каждый понимает по-своему, а маппинга нет — у вас де-факто один контекст, а вы пытаетесь делать вид, что их несколько.
  • Слишком много shared kernel. Если в общем модуле живёт половина модели, контексты неотделимы. Это не контексты, это слои.

Эволюция контекстов

Контексты не вырубаются один раз. По мере роста системы они появляются, делятся, объединяются. Я наблюдал три типичных пути.

Первый — выделение нового контекста. Внутри одного начинают расти разные подмодели (например, в продуктовом контексте отдельно эволюционирует «продукт для каталога» и «продукт для рекомендаций»). Сначала это разные пакеты, потом разные модули, потом разные сервисы. Это здоровый путь, если каждый шаг сопровождается явным контрактом.

Второй — слияние двух контекстов. Бывает, когда понимание устаканилось, и оказалось, что разделение было искусственным. Слияние делается аккуратно: сначала команды объединяются, потом модели сближаются, потом код. Если начать с кода — получишь монолит с двумя половинами, которые продолжают жить как раньше.

Третий — деградация. Контекст теряет владельца, в нём накапливается каша, и через год это превращается в shared infrastructure без чёткой ответственности. Лучшее средство — вовремя зафиксировать «у этого контекста нет команды» и решить, что с ним делать: расформировать или передать.

Чек-лист при выделении контекста

  • У контекста есть один основной владелец (команда).
  • Внутри контекста используется один язык: одно слово — одно значение.
  • На границе контекста есть явный контракт: REST, события, схема, ACL.
  • Изменения внутри не требуют согласования с соседями (за исключением контракта).
  • Транзакции не пересекают границу. Если данные нужны соседу — копия через события или вызов.
  • Команда может релизить независимо от соседей.

Что запомнить

Bounded context — инструмент управления сложностью на уровне людей и понятий, а не на уровне технологий. Сначала поймите, какие у вас языки и кто говорит на каждом. Потом нарисуйте, как они соотносятся. И только потом превращайте это в модули и сервисы. Если идти в обратную сторону — от сервисов к контекстам — получится привычная распределённая система, в которой все спорят о смысле слова «заказ» по три раза в неделю.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.