← назад к разделу

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

Стратегические паттерны DDD — это инструменты для наведения порядка на уровне всей системы: как разделить её на части, как назвать вещи своими именами и как организовать взаимодействие между частями.

одно слово «заказ» — две модели по разные стороны границы контекст «Продажи» Заказ A-1743 позиции: 3 шт скидка: 10 % промокод: SALE адрес: Тверская 7 контекст «Доставка» Заказ A-1743 зона: —вес: —мест: — курьер: назначает сама граница контекстов зона: Центрвес: 4,2 кгмест: 2 как не надо: отдать заказ как естьзаказ из Продажкак естьскидка и промокод в Доставке ничего не значат,а зона, вес и число мест так и не посчитаны как надо: перевод на границезаказ из Продажпереводпозиции → вес, местадрес → зонаскидка, промокод → неттолько свои поля

Слово «заказ» звучит одинаково по обе стороны границы, а набор полей за ним разный. Поэтому данные проходят границу переводом: «позиции» превращаются в вес и число мест, «адрес» — в зону доставки, а «скидка» и «промокод» не переходят вовсе — в Доставке им нечего означать.

Обязательно

Единый язык (Ubiquitous Language)спросят на собеседовании

Представьте: бизнес говорит «платёжный спор», аналитик пишет «претензия», а в коде — PaymentProblem, Claim и Dispute вперемешку. Каждый «перевод» — потенциальная ошибка. Разработчик реализует не то, что задумал бизнес, потому что неверно понял термин.

Ubiquitous Language — общий словарь терминов, который используют и бизнес, и разработчики во всём: в разговорах, документации и коде.

Договорились называть «платёжный спор» словом Dispute — и это слово появляется везде:

// Плохо: термин взят из головы разработчика
class PaymentProblem {
    void resolve() { /* ... */ }
}

// Хорошо: термин из бизнес-словаря
class Dispute {
    private DisputeStatus status;

    void accept() {
        this.status = DisputeStatus.ACCEPTED;
    }

    void reject(String reason) {
        this.status = DisputeStatus.REJECTED;
    }
}

Когда код читается как описание бизнес-процесса — новые люди быстро входят в контекст, а на ревью сразу видно, если что-то не соответствует договорённостям.

Как формировать единый язык:

  • составить глоссарий ключевых терминов вместе с бизнесом;
  • если термин неоднозначен — выбрать один вариант и зафиксировать (в вики, ADR или README);
  • на ревью проверять, что новый код использует слова из глоссария;
  • обновлять глоссарий, когда появляются новые понятия.

Важный момент: единый язык действует только внутри одного контекста. В разных частях системы одно и то же слово может означать разное — и это нормально. Именно поэтому нужны Bounded Contexts.

Bounded Context — граница, внутри которой модель не врётспросят на собеседовании

Слово «клиент» в биллинге — это юридическое лицо с ИНН и адресом счёта. В службе поддержки — это пользователь с открытыми тикетами. В маркетинге — участник программы лояльности с баллами.

Если пытаться описать всё это одним классом Client, он обрастает полями из всех контекстов сразу:

// Антипаттерн: один класс для всех контекстов
class Client {
    private String inn;            // нужно биллингу
    private String billingAddress; // нужно биллингу
    private String email;          // нужно поддержке
    private int openTickets;       // нужно поддержке
    private String loyaltyLevel;   // нужно маркетингу
    private int bonusPoints;       // нужно маркетингу
}

Такой класс — «Бог-объект». Любое изменение в нём может сломать что-то неожиданное.

Bounded Context — явная граница, внутри которой модель и единый язык имеют чёткий, непротиворечивый смысл. За пределами этой границы то же слово может значить другое.

// Billing Context: Client — это плательщик
package com.example.billing.domain;

class Client {
    private final ClientId id;
    private final String inn;
    private final Address billingAddress;
    private PaymentMethod preferredPayment;
}

Второй Client — отдельный файл в отдельном пакете, в одном файле два package не живут:

// Support Context: Client — это пользователь с тикетами
package com.example.support.domain;

class Client {
    private final ClientId id;
    private final String email;
    private final List<TicketId> openTickets;

    boolean hasOpenTickets() {
        return !openTickets.isEmpty();
    }
}

Обратите внимание на List<TicketId>, а не List<Ticket>: тикет живёт своей жизнью и меняется отдельно от клиента, поэтому клиент держит только его номер. Подробнее про это правило — в тактических паттернах.

Два разных класса с одним именем — это не дублирование, а нормальный DDD. Каждый класс несёт только те поля, которые важны в его контексте.

Как понять, что нужна граница:

  • разные команды имеют в виду разное, говоря одно слово;
  • одна часть системы меняется часто, другая стабильна;
  • над разными частями работают разные команды.

Bounded Context — это не микросервис. Это логическая граница модели, а микросервис — физическая граница деплоя. На старте проекта один микросервис может содержать несколько контекстов, разделённых пакетами.

Один сервис два контекста в пакетах Граница держится полгода без парных правок Два сервиса разрез по той же линии

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

Симптом, который видно в коде и в базе

Признаки выше — про людей и темп. Есть ещё один, самый надёжный, потому что он не требует ничьих мнений: посмотреть на главную сущность.

Граница нужна, когда у одного класса или одной таблицы появляются:

  • Поля-флаги, значащие «это для другого отдела»: isWarehouseVisible, accountingCode, marketingSegment, deliveryNote. Каждое такое поле — след того, что сюда пришло понятие из другой области и не нашло себе места.
  • Поля, которые заполнены у половины записей. Колонка, где NULL у 60 % строк, обычно значит, что там два разных понятия в одной таблице: например, заказ на доставку и заказ на самовывоз, у которых разные обязательные данные.
  • Методы, которые в половине случаев кидают исключение. order.assignCourier() осмыслен только для доставки; order.markAsPickedUp() — только для самовывоза. Класс, где половина методов применима к половине объектов, — два класса в одном.
  • Статусы из разных процессов в одном перечислении: CREATED, PAID, PACKED, SHIPPED, INVOICED, RECONCILED. Первые два — оформление, следующие два — склад, последние два — бухгалтерия. Три процесса в одной статусной модели, и каждый отдел знает только свой участок.
  • Проверки, зависящие от того, кто вызывает: «если это склад, то поле обязательно, а если кабинет покупателя, то нет». Условие «кто вызывает» внутри домена — прямое указание на то, что здесь нужна граница, а не условие.
  • Сущность, которую нельзя изменить, не спросив три команды. Это уже не симптом, а диагноз.

Почему этот признак лучше остальных: он измеримый и не спорный. Можно открыть класс Order, посчитать поля и разложить их по отделам — и, если получилось три группы, разговор о границах перестаёт быть теоретическим. Заодно это готовый материал для разговора с бизнесом: «вот эти шесть полей нужны только складу, и меняет их только склад» — убедительнее любых слов про ограниченные контексты.

Практический приём: выписать все поля главной сущности в таблицу и пометить, кто их читает и кто меняет. Занимает полчаса, и границы обычно проступают сами.

Поддомены: где вкладывать силыспросят на собеседовании

Команда полгода вылизывала модуль рассылок, а алгоритм ранжирования — то, ради чего к продукту приходят, — собрали на коленке. Не все части системы одинаково важны, и DDD делит предметную область на три типа поддоменов, чтобы усилия шли туда, где они окупаются.

Core Domain — то, что даёт бизнесу конкурентное преимущество. Здесь зарабатывают деньги. Именно сюда нужна самая сложная и тщательная доменная модель, лучшие разработчики, максимальное покрытие тестами.

Supporting Subdomain — важно для работы, но не уникально. Без него не обойтись, но конкуренты делают то же самое. Можно применять более простые подходы: CRUD с валидацией достаточно.

Generic Subdomain — типовые задачи: авторизация, уведомления, выставление счетов, хранение файлов. Обычно выгоднее купить готовое решение или подключить открытую библиотеку, чем писать самостоятельно.

Пример для маркетплейса:

  • Core: каталог и ранжирование, оформление заказа, расчёты с продавцами, динамическое ценообразование. Здесь нужна сильная доменная модель с инвариантами.
  • Supporting: личный кабинет покупателя, кабинет продавца, отзывы и рейтинги, споры по заказам. Нужны, но упрощённых моделей достаточно.
  • Generic: SMS/e-mail уведомления, проверки на мошенничество, хранилище медиафайлов товаров. Покупаем как сервис или подключаем готовое.

Четыре вопроса, которые отличают Core от остального

«Core — там, где зарабатывают» звучит очевидно до первого разговора, где бизнес говорит «мы зарабатываем на всём». Нужны проверяемые вопросы, и их четыре.

1. Что будет, если делать это вдвое хуже конкурентов? Core: клиенты уйдут — это то, ради чего к нам приходят. Supporting: неудобно, но терпимо. Generic: никто не заметит, потому что все делают одинаково. Вопрос беспощадный и работает лучше всех остальных, потому что «зарабатываем на всём» разбивается о него мгновенно: подбор маршрута вдвое хуже — уйдут; форма обратной связи вдвое хуже — не уйдут.

2. Можно ли это купить? Если на рынке есть готовое решение, которым пользуются и конкуренты, — это Generic по определению, каким бы важным оно ни было. Уведомления критичны для продукта, и их покупают. Обратное тоже верно: то, что нельзя купить, потому что такого ни у кого нет, — сильный кандидат в Core.

3. Переживёт ли бизнес отключение этого на день? Здесь измеряется не уникальность, а критичность, и это другая ось. Платёжный шлюз критичен (день без оплат — катастрофа) и при этом Generic: его покупают, и уникальности в нём нет. Полезно держать обе оси в голове: критичность решает, сколько вкладывать в надёжность, уникальность — сколько вкладывать в модель. Путаница этих осей — самая частая ошибка деления.

4. Кто принимает решения об этом куске? Core: сам продукт, с гипотезами и экспериментами; здесь бизнес спорит о деталях и меняет правила каждый месяц. Supporting: требования приходят готовыми и меняются редко. Generic: требования вообще не наши, они отраслевые. Признак на практике: там, где бизнес хочет обсуждать детали, там и Core — это самый честный индикатор, потому что он не зависит от чужих слов.

Практика проведения: вопросы задают по каждому куску вслух и с ответом бизнеса, а не решают в кабинете. Занимает час, и результат записывают: кусок, ответы, вывод. Через полгода к этой таблице возвращаются — деление меняется вместе с бизнесом.

И честная оговорка: Core должен быть маленьким. Если в Core попало восемь модулей из десяти, деление не сделано: смысл его в том, чтобы направить лучших людей и самую тщательную модель в одно-два места. Два Core на систему — нормально, пять — признак, что вопросы задавали не всерьёз.

Деление зависит от бизнеса, а не от названия модуля. Проверки на мошенничество попали в Generic, потому что эта площадка берёт готовый сервис проверок. Там, где потери от мошенничества считают процентами от оборота, антифрод переезжает в Supporting, а то и в Core — со своей моделью, своей командой и своими экспериментами.

Три полосы поддоменов: Core с каталогом, оформлением заказа, расчётами и ценами, Supporting с кабинетами, отзывами и спорами, Generic с уведомлениями, фискализацией, антифродом и хранилищем

Context Map — карта зависимостей между командамиспросят на собеседовании

О ломающем изменении в соседнем контексте команда узнаёт из упавшего прода, а не из чьего-то сообщения — потому что нигде не записано, кто от кого зависит. Когда контексты определены, эту зависимость фиксируют явно. Context Map — это схема всех Bounded Contexts и связей между ними. Не диаграмма классов, а карта ответственности: кто кому поставляет данные и на каких условиях.

Карта контекстов: Orders отдаёт данные Payments по OHS/PL и Shipping по Customer-Supplier, Accounting подстраивается как Conformist, а внешние шлюз и доставка закрыты ACL

На этой карте видно:

  • Orders предоставляет данные Payments через Open Host Service — публичный API со стабильным контрактом.
  • Payments защищается от внешнего платёжного шлюза через Anti-Corruption Layer — переводит чужую модель в свою.
  • Accounting принимает модель Payments как есть (Conformist) — адаптируется под поставщика. Так вышло исторически: у Accounting нет рычага, чтобы диктовать условия Payments. Цена известна — любое изменение модели Payments прилетает в Accounting без предупреждения, и однажды дешевле будет поставить свой перевод на границе.
  • Shipping получает данные от Orders по условию Customer–Supplier — Orders учитывает потребности Shipping при изменениях.
  • Shipping точно так же закрывается Anti-Corruption Layer от внешней службы доставки — её статусы и коды ошибок не должны протекать внутрь.

Карта помогает обнаружить проблемные зависимости до того, как они попадут в код.

Кто её рисует, где она лежит и когда меняется

Картинка бесполезна без ответа на эти три вопроса — а именно они решают, будет карта работать или устареет за месяц.

Где лежит: в репозитории, как текст, а не как картинка. Рабочая форма — таблица связей в файле, и картинка из неё, а не вместо неё:

| Откуда | Куда | Тип связи | Что передаётся | Владелец контракта |
|---|---|---|---|---|
| Orders | Payments | Open Host Service | запрос на оплату, статус | Orders |
| Payments | внешний шлюз | Anti-Corruption Layer | наш формат ↔ их формат | Payments |
| Accounting | Payments | Conformist | модель Payments как есть | Payments |
| Orders | Shipping | Customer–Supplier | заказ к отправке | Orders, с учётом Shipping |

Текстовая форма даёт то, чего не даёт картинка: она правится пул-реквестом, её изменения видны в истории, и по ней можно искать («кто зависит от Payments?» — поиск по слову). Картинку рисуют для разговора и перерисовывают из таблицы.

Кто ведёт: один человек с именем. Обычно архитектор или ведущий инженер платформы — тот, у кого это в обязанностях. Без имени карта не обновляется никогда: каждая отдельная команда видит только свои связи и не считает своим делом чужие.

Когда меняется — по событиям, а не по расписанию. Поводы пересмотреть, и их стоит перечислить явно, потому что «когда понадобится» не работает:

  • Появилась новая связь между контекстами. Правило простое и жёсткое: изменение, которое добавляет обращение к другому контексту, не проходит ревью без строки в карте. Это единственный механизм, который держит карту живой.
  • Изменился тип связи. Accounting поставил свой переводчик — был Conformist, стал ACL. Это событие, которое надо записать: тип связи описывает договорённость между командами, а она изменилась.
  • Контекст появился, исчез или разделился.
  • Сменился владелец контракта — например, команда передала сервис другой команде.
  • Раз в квартал — сверка. Пройти по таблице и спросить «эта связь ещё существует?». Обычно находится две-три мёртвых связи (код удалён, строка осталась) и одна живая, которой в карте нет.

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

Стратегическая дистилляция

Дистилляция — процесс выделения самого важного в домене. Цель: понять, где настоящая бизнес-ценность, и направить туда усилия.

Без дистилляции команда тратит одинаково много времени на модуль рассылок и на ключевой алгоритм ранжирования. После дистилляции приоритеты становятся явными.

Domain Vision Statement — короткий документ (1–2 абзаца): что является Core Domain, какую ценность он несёт, чем отличается от конкурентов.

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

Highlighted Core — явное указание, какие модули составляют Core Domain. Структура каталогов — один из способов зафиксировать это решение:

src/
├── core/           ← Core Domain: максимум внимания
│   ├── routing/    ← подбор маршрутов — ключевая ценность
│   └── pricing/    ← динамическое ценообразование
├── supporting/     ← Supporting: нужно, но не уникально
│   ├── crm/
│   └── reporting/
└── generic/        ← Generic: готовые решения
    ├── notification/
    └── auth/

Как проводят дистилляцию

Два документа выше — результат, а не процесс. Сам разговор устроен так.

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

По какому признаку спорят. По четырём вопросам из раздела про поддомены, и вслух по каждому куску. Спор идёт не «важно ли это» (важно всё), а «что будет, если делать это вдвое хуже конкурентов». Ведущий возвращает разговор к этой формулировке каждый раз, когда он уходит в «нам нужно и то, и это».

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

  1. Заставить сравнить, а не оценить. Не «это Core?», а «если выбирать, куда поставить двух лучших инженеров на квартал — сюда или сюда?». Сравнение работает там, где абсолютная оценка не работает никогда.
  2. Назвать цену. «Core означает свою модель, свою команду и экспериментальную разработку — это примерно вот столько людей на каждый кусок. У нас столько нет, значит, выбираем два». Ограниченность ресурса — единственный аргумент, который заканчивает этот спор.
  3. Разделить оси. Обычно спор вызван смешением уникальности и критичности (см. вопрос 3 выше): бизнес называет Core то, без чего система не работает. Разложив куски по двум осям на доске, обе стороны обычно соглашаются быстро.

Чем заканчивается. Двумя артефактами (заявление о ценности и явная разметка модулей) и — самое важное — решением о людях: кто работает на Core. Дистилляция без этого решения ничего не меняет, потому что приоритет, не выраженный в распределении людей, не приоритет.

Остальные приёмы дистилляции

У Эванса приёмов больше двух, и остальные отвечают на вопрос «что делать, когда Core найден, но утонул в остальном коде».

Согласованные механизмы (Cohesive Mechanisms). В домене часто есть сложный алгоритм, который к предметной области относится косвенно: расчёт маршрута по графу, подбор оптимальной раскладки по коробкам, движок правил. Его выносят в отдельный модуль с ясным интерфейсом («посчитай мне маршрут») — и тогда доменная модель говорит о домене, а не о алгоритмах. Признак, что пора: код, в котором на десять строк предметной области приходится сто строк вычислений.

Отделённое ядро (Segregated Core). Core найден, но перемешан с поддерживающим кодом в одних классах. Приём прямой: вынести Core в отдельный модуль, оставив в старом месте то, что поддерживает. Работа механическая и болезненная (много правок импортов), зато после неё видно, какого размера ядро на самом деле — и обычно оно оказывается меньше, чем думали.

Абстрактное ядро (Abstract Core). Для больших систем: в ядре оставляют интерфейсы и главные понятия, а конкретные реализации уезжают в модули. Тогда ядро читается целиком за час, и по нему понятно устройство домена. Приём дорогой и оправдан, когда над одним доменом работают несколько команд.

Порядок применения на практике: сначала заявление о ценности и разметка (дёшево, даёт приоритеты), потом согласованные механизмы (обычно самая быстрая победа), потом отделённое ядро, если Core действительно перемешан. Абстрактное ядро — редко и только на больших системах.

Big Ball of Mud — что происходит без стратегии

Big Ball of Mud — система без явных границ: модели перемешаны, любой модуль обращается к таблицам другого напрямую, изменение в одном месте ломает непредсказуемые вещи в другом.

Признаки:

  • один класс User или Order используется во всей системе;
  • нет чётких границ ответственности между командами;
  • никто не знает, какой эффект у изменения.

Полностью переписать такую систему обычно невозможно. Практичный путь — постепенная декомпозиция:

  1. Найти Core Domain — самую ценную часть.
  2. Выделить для неё Bounded Context — вынести в отдельный пакет или модуль.
  3. Поставить Anti-Corruption Layer — защитить новый чистый контекст от остального кода.
  4. Повторить для следующего по важности контекста.

Big Ball of Mud — не приговор, а отправная точка.

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

Так выглядит клубок после первых трёх шагов: остальной код обращается к вынесенному ядру только через слой перевода, и прямой стрелки внутрь нет ни одной.

Где взять время и как объяснить бизнесу

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

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

Назвать цену бездействия в прошедшем времени. Не «у нас плохая архитектура», а «за последний квартал три раза изменение в кабинете покупателя ломало склад; на разбор ушло четыре дня, один инцидент был в проде». Числа из прошлого — единственный аргумент, который слышит бизнес, и он у вас есть: посмотрите последние аварии и оценки задач.

Взять из бюджета на надёжность, а не на функции. Разговор про границы часто идёт легче, если он не про «архитектуру», а про конкретный риск: «это тот кусок, из-за которого падал прод». Тогда это не улучшение, а починка.

Фиксированная доля времени. Договорённость «20 % времени команды идёт на техническую работу» работает, только если её объявили заранее и она защищена. Без защиты первый же срочный запрос эту долю съедает — и больше не возвращает.

Чего не делать: просить «квартал на рефакторинг» (не дадут, а если дадут, то с ожиданием видимого результата, которого не будет), делать по ночам молча (работа без владельца и без обсуждения), и начинать с самого запутанного куска ради эффекта.

Границу провели неправильно: как понять и что делать

Границы — решение, а решения бывают неверными. Обнаруживается это через полгода, и по узнаваемым признакам:

  • Цепочки синхронных вызовов между контекстами. Чтобы ответить на один запрос, контекст A зовёт B, тот зовёт C. Признак того, что данные, нужные вместе, оказались по разные стороны границы.
  • «Пробросьте нам ещё одно поле». Просьба, повторяющаяся раз в месяц, означает, что граница режет понятие пополам: одному контексту постоянно нужны детали другого.
  • Изменения всегда парные. Любая задача требует правок в двух контекстах и согласованного выката. Это самый надёжный признак: граница, которую нельзя менять по одну сторону, — не граница.
  • Общая таблица или общая библиотека с моделью. Появилась, чтобы «не дублировать», и теперь склеивает контексты крепче, чем до разделения.
  • Одно и то же понятие живёт по обе стороны с одинаковым смыслом. Не разные смыслы (это нормально и есть суть границ), а буквально одно и то же — значит, разделили там, где делить было нечего.

Что делают дальше. Три варианта, и выбор между ними — не вкус:

  1. Сдвинуть границу. Самый частый ответ: понятие, которое постоянно требуют пробросить, переезжает туда, где оно нужнее, вместе со своими правилами. Работа на дни-недели и обычно решает проблему целиком.
  2. Слить контексты обратно. Если изменения всегда парные, а понятия одинаковы, разделение было ошибкой: два контекста становятся одним. Это не позор и не откат к беспорядку — это исправление решения, принятого при меньшем знании. В монолите с модулями это дёшево; если контексты успели стать сервисами, дорого — и именно поэтому границы сначала проводят внутри одного приложения.
  3. Оставить и поставить переводчик. Когда сдвинуть нельзя (разные команды, разные сроки), на границе ставят защитный слой и фиксируют контракт: пусть связь останется, но она перестанет протекать. Это не лечение, а обезболивание, и его выбирают осознанно.

И самое полезное правило на будущее: провести границу обратимо. Пакеты и модули внутри одного приложения сдвигаются за день; границы, застывшие в отдельных сервисах с собственными базами, — за квартал. Поэтому первую версию границ делают внутри приложения, а на сервисы разрезают то, что прожило полгода без парных изменений.

Дополнительно: при первом чтении можно пропустить

Глубже: первая граница в живой системерасширенное

Статья описывает контексты как результат. Настоящий вопрос звучит иначе: система уже есть, в ней один OrderService на три тысячи строк и общая база, и куда провести первую границу, не остановив разработку.

Где резать первой. Не там, где проще, а там, где больнее: поддомен с самой высокой частотой изменений и самой высокой ценой ошибки, обычно ядро (оформление заказа, расчёт цены, платежи). Признак правильного места: за последний квартал больше половины инцидентов и правок пришлись сюда. Второй кандидат это место, где один термин уже означает два разных понятия, о чём раздел про контексты: там граница напрашивается сама.

Как новый контекст живёт со старым кодом. Внутри монолита, как модуль со своим пакетом, своей моделью и явным API: старый код зовёт новый через интерфейс, а новый читает старые таблицы через слой перевода (тот же антикоррупционный слой, что и между сервисами, о нём следующая статья). Общие таблицы на время переезда остаются у старого кода на запись, новый пишет в свои; сначала уводят логику, потом данные, и это те же шаги, что при разрезании сервисов. Ни один шаг не требует «большого переписывания»: каждая правка это перенос одного правила из старого места в модель нового контекста с тестом.

Как объяснить бизнесу. «Неделя без новых функций» это плохой разговор, и его можно не вести: первый срез нового контекста выпускает настоящее изменение, которое бизнес просил (новое правило скидки, новый статус заказа), просто сделанное уже в новой модели. Аргумент для руководителя не «мы внедряем DDD», а «правки в оформлении заказа сейчас занимают неделю и ломают соседей; после переезда ядра это станет днём, и вот первая такая правка». Цена переезда измеряется теми же метриками поставки, что и всё остальное.

Чего не делать. Не выделять сервис раньше, чем граница прожила в монолите полгода; не переписывать всё в новой модели сразу; не заводить второй контекст, пока первый не устоялся; и не считать переезд законченным, пока старая модель ещё пишет в те же таблицы, о чём статья про монолит и микросервисы.

Глубже: единственный перевод, который остаётся: словарь русских терминов и английских имёнрасширенное

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

Глоссарий как файл в репозитории. Таблица из трёх колонок: термин по-русски, идентификатор в коде, определение в одну строку; плюс запрещённые синонимы («заявка, не запрос и не обращение»). Лежит рядом с кодом контекста (docs/glossary.md), а не на вики, потому что меняется в тех же pull request, что и код, и ревью проверяет: новое понятие в коде появилось вместе со строкой в глоссарии. Из того же файла берут имена таблиц и колонок, полей API и событий, и это снимает спор «customer или client» один раз.

Правила перевода. Одно русское понятие, один английский идентификатор, во всех слоях одинаково: Application в домене, application в таблице, applicationId в API. Транслитерация (Zayavka) запрещена: она не читается ни бизнесом, ни новым разработчиком, ни моделью, которая генерирует код. Когда английского слова с тем же смыслом нет (внутренний термин компании), выбирают ближайшее и пишут в глоссарий, что оно означает именно ваше понятие. Переименование термина это отдельное изменение с записью решения (ADR) и переименованием во всех слоях сразу, а не «в новом коде так, в старом иначе».

Кто держит в согласии. Владелец глоссария это команда контекста, а не аналитик и не архитектор; на ревью спрашивают «это слово из глоссария?», а тест на архитектуру ловит запрещённые синонимы в именах классов по списку. Интерфейс пользователя показывает русские термины из того же словаря через файлы локализации, и когда бизнес меняет слово, меняется строка перевода, а не идентификатор. Тот же файл читает и агент, который пишет код по спецификации, о чём статья про онтологию и модель.

Коротко

  • Ubiquitous Language держится только внутри Bounded Context: за границей то же слово законно значит другое, поэтому договариваться о словах имеет смысл там, где граница уже проведена, а не по всей системе сразу.
  • Граница модели и граница деплоя это разные вещи: один сервис спокойно держит два контекста в пакетах. Деление на Core Domain, Supporting и Generic отвечает не на вопрос «что важно», а на вопрос, куда идут лучшие люди и самая тщательная модель.
  • Context Map и стратегическая дистилляция нужны затем, чтобы проблемная зависимость и место приложения сил стали видны до того, как попадут в код; Big Ball of Mud от этого сразу не исчезает, но разбирается по одному контексту, и каждый вынесенный кусок закрывают Anti-Corruption Layer.
  • Первую границу в живой системе проводят по самому больному поддомену, внутри монолита модулем с явным API и слоем перевода к старым таблицам; первый срез выпускает настоящее изменение, а не «внедрение DDD». Перевод «заявка → Application» остаётся всегда: глоссарий в репозитории с термином, идентификатором и определением, один термин во всех слоях, без транслитерации, переименование через ADR.
  • Core отделяют четырьмя вопросами: что будет, если делать вдвое хуже конкурентов, можно ли купить, переживёт ли бизнес день без этого, где бизнес спорит о деталях; критичность и уникальность — разные оси, а Core должен быть маленьким.
  • Самый надёжный признак нужной границы виден в коде: поля-флаги для другого отдела, колонки с половиной пустых значений, методы, кидающие исключение в половине случаев, статусы трёх процессов в одном перечислении.
  • Карта контекстов живёт таблицей в репозитории с владельцем контракта, ведёт её один человек, а поддерживается правилом «новая связь не проходит ревью без строки в карте».
  • Дистилляцию проводят сравнением, а не оценкой: «куда поставить двух лучших на квартал», с названной ценой Core и разделением осей; приёмы сверх двух — согласованные механизмы, отделённое и абстрактное ядро.
  • Время на вынос контекста берут, прицепив его к задаче, которая и так нужна, и называют цену бездействия числами из прошлых аварий; квартал на рефакторинг не просят.
  • Неверная граница видна по парным изменениям, цепочкам синхронных вызовов и просьбам «пробросьте поле»; лечится сдвигом границы, слиянием контекстов обратно или переводчиком, поэтому первые границы делают обратимыми внутри приложения.

Что почитать дальше