Когда проект маленький — один разработчик держит всё в голове. Но как только команда вырастает и система усложняется, начинается путаница: одни называют сущность «заявкой», другие — «запросом», третьи — «ордером». Один модуль тянет данные из другого напрямую, и никто толком не знает, где заканчивается «продажи» и начинается «склад».
Стратегические паттерны DDD — это инструменты для наведения порядка на уровне всей системы: как разделить её на части, как назвать вещи своими именами и как организовать взаимодействие между частями.
Слово «заказ» звучит одинаково по обе стороны границы, а набор полей за ним разный. Поэтому данные проходят границу переводом: «позиции» превращаются в вес и число мест, «адрес» — в зону доставки, а «скидка» и «промокод» не переходят вовсе — в Доставке им нечего означать.
Единый язык (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 — со своей моделью, своей командой и своими экспериментами.
Context Map — карта зависимостей между командамиспросят на собеседовании
О ломающем изменении в соседнем контексте команда узнаёт из упавшего прода, а не из чьего-то сообщения — потому что нигде не записано, кто от кого зависит. Когда контексты определены, эту зависимость фиксируют явно. Context Map — это схема всех Bounded Contexts и связей между ними. Не диаграмма классов, а карта ответственности: кто кому поставляет данные и на каких условиях.
На этой карте видно:
- 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 всё сразу. Ситуация не исключение, а норма, и есть три приёма, которые её разрешают:
- Заставить сравнить, а не оценить. Не «это Core?», а «если выбирать, куда поставить двух лучших инженеров на квартал — сюда или сюда?». Сравнение работает там, где абсолютная оценка не работает никогда.
- Назвать цену. «Core означает свою модель, свою команду и экспериментальную разработку — это примерно вот столько людей на каждый кусок. У нас столько нет, значит, выбираем два». Ограниченность ресурса — единственный аргумент, который заканчивает этот спор.
- Разделить оси. Обычно спор вызван смешением уникальности и критичности (см. вопрос 3 выше): бизнес называет Core то, без чего система не работает. Разложив куски по двум осям на доске, обе стороны обычно соглашаются быстро.
Чем заканчивается. Двумя артефактами (заявление о ценности и явная разметка модулей) и — самое важное — решением о людях: кто работает на Core. Дистилляция без этого решения ничего не меняет, потому что приоритет, не выраженный в распределении людей, не приоритет.
Остальные приёмы дистилляции
У Эванса приёмов больше двух, и остальные отвечают на вопрос «что делать, когда Core найден, но утонул в остальном коде».
Согласованные механизмы (Cohesive Mechanisms). В домене часто есть сложный алгоритм, который к предметной области относится косвенно: расчёт маршрута по графу, подбор оптимальной раскладки по коробкам, движок правил. Его выносят в отдельный модуль с ясным интерфейсом («посчитай мне маршрут») — и тогда доменная модель говорит о домене, а не о алгоритмах. Признак, что пора: код, в котором на десять строк предметной области приходится сто строк вычислений.
Отделённое ядро (Segregated Core). Core найден, но перемешан с поддерживающим кодом в одних классах. Приём прямой: вынести Core в отдельный модуль, оставив в старом месте то, что поддерживает. Работа механическая и болезненная (много правок импортов), зато после неё видно, какого размера ядро на самом деле — и обычно оно оказывается меньше, чем думали.
Абстрактное ядро (Abstract Core). Для больших систем: в ядре оставляют интерфейсы и главные понятия, а конкретные реализации уезжают в модули. Тогда ядро читается целиком за час, и по нему понятно устройство домена. Приём дорогой и оправдан, когда над одним доменом работают несколько команд.
Порядок применения на практике: сначала заявление о ценности и разметка (дёшево, даёт приоритеты), потом согласованные механизмы (обычно самая быстрая победа), потом отделённое ядро, если Core действительно перемешан. Абстрактное ядро — редко и только на больших системах.
Big Ball of Mud — что происходит без стратегии
Big Ball of Mud — система без явных границ: модели перемешаны, любой модуль обращается к таблицам другого напрямую, изменение в одном месте ломает непредсказуемые вещи в другом.
Признаки:
- один класс
UserилиOrderиспользуется во всей системе; - нет чётких границ ответственности между командами;
- никто не знает, какой эффект у изменения.
Полностью переписать такую систему обычно невозможно. Практичный путь — постепенная декомпозиция:
- Найти Core Domain — самую ценную часть.
- Выделить для неё Bounded Context — вынести в отдельный пакет или модуль.
- Поставить Anti-Corruption Layer — защитить новый чистый контекст от остального кода.
- Повторить для следующего по важности контекста.
Big Ball of Mud — не приговор, а отправная точка.
Так выглядит клубок после первых трёх шагов: остальной код обращается к вынесенному ядру только через слой перевода, и прямой стрелки внутрь нет ни одной.
Где взять время и как объяснить бизнесу
Четыре шага выше не выполняются не потому, что непонятны, а потому, что на них не дают времени: функций не прибавилось, а неделя ушла. Способы, которые работают, — по убыванию надёжности.
Прицепить к задаче, которая и так нужна. Самый надёжный и почти единственный работающий на практике: бизнес просит изменение в области платежей — в той же задаче выносится контекст платежей. Оценка задачи включает вынос, и отдельного разговора не требуется. Правило, которое из этого следует: границу проводят там, куда уже пришла работа, а не там, где хочется. Это заодно снимает риск вынести не то: если в этот кусок идут изменения, он точно живой.
Назвать цену бездействия в прошедшем времени. Не «у нас плохая архитектура», а «за последний квартал три раза изменение в кабинете покупателя ломало склад; на разбор ушло четыре дня, один инцидент был в проде». Числа из прошлого — единственный аргумент, который слышит бизнес, и он у вас есть: посмотрите последние аварии и оценки задач.
Взять из бюджета на надёжность, а не на функции. Разговор про границы часто идёт легче, если он не про «архитектуру», а про конкретный риск: «это тот кусок, из-за которого падал прод». Тогда это не улучшение, а починка.
Фиксированная доля времени. Договорённость «20 % времени команды идёт на техническую работу» работает, только если её объявили заранее и она защищена. Без защиты первый же срочный запрос эту долю съедает — и больше не возвращает.
Чего не делать: просить «квартал на рефакторинг» (не дадут, а если дадут, то с ожиданием видимого результата, которого не будет), делать по ночам молча (работа без владельца и без обсуждения), и начинать с самого запутанного куска ради эффекта.
Границу провели неправильно: как понять и что делать
Границы — решение, а решения бывают неверными. Обнаруживается это через полгода, и по узнаваемым признакам:
- Цепочки синхронных вызовов между контекстами. Чтобы ответить на один запрос, контекст A зовёт B, тот зовёт C. Признак того, что данные, нужные вместе, оказались по разные стороны границы.
- «Пробросьте нам ещё одно поле». Просьба, повторяющаяся раз в месяц, означает, что граница режет понятие пополам: одному контексту постоянно нужны детали другого.
- Изменения всегда парные. Любая задача требует правок в двух контекстах и согласованного выката. Это самый надёжный признак: граница, которую нельзя менять по одну сторону, — не граница.
- Общая таблица или общая библиотека с моделью. Появилась, чтобы «не дублировать», и теперь склеивает контексты крепче, чем до разделения.
- Одно и то же понятие живёт по обе стороны с одинаковым смыслом. Не разные смыслы (это нормально и есть суть границ), а буквально одно и то же — значит, разделили там, где делить было нечего.
Что делают дальше. Три варианта, и выбор между ними — не вкус:
- Сдвинуть границу. Самый частый ответ: понятие, которое постоянно требуют пробросить, переезжает туда, где оно нужнее, вместе со своими правилами. Работа на дни-недели и обычно решает проблему целиком.
- Слить контексты обратно. Если изменения всегда парные, а понятия одинаковы, разделение было ошибкой: два контекста становятся одним. Это не позор и не откат к беспорядку — это исправление решения, принятого при меньшем знании. В монолите с модулями это дёшево; если контексты успели стать сервисами, дорого — и именно поэтому границы сначала проводят внутри одного приложения.
- Оставить и поставить переводчик. Когда сдвинуть нельзя (разные команды, разные сроки), на границе ставят защитный слой и фиксируют контракт: пусть связь останется, но она перестанет протекать. Это не лечение, а обезболивание, и его выбирают осознанно.
И самое полезное правило на будущее: провести границу обратимо. Пакеты и модули внутри одного приложения сдвигаются за день; границы, застывшие в отдельных сервисах с собственными базами, — за квартал. Поэтому первую версию границ делают внутри приложения, а на сервисы разрезают то, что прожило полгода без парных изменений.
Глубже: первая граница в живой системерасширенное
Статья описывает контексты как результат. Настоящий вопрос звучит иначе: система уже есть, в ней один 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 и разделением осей; приёмы сверх двух — согласованные механизмы, отделённое и абстрактное ядро.
- Время на вынос контекста берут, прицепив его к задаче, которая и так нужна, и называют цену бездействия числами из прошлых аварий; квартал на рефакторинг не просят.
- Неверная граница видна по парным изменениям, цепочкам синхронных вызовов и просьбам «пробросьте поле»; лечится сдвигом границы, слиянием контекстов обратно или переводчиком, поэтому первые границы делают обратимыми внутри приложения.
Что почитать дальше
- Что такое DDD и зачем он нужен — если не читали: с чего начинается доменно-ориентированное проектирование.
- Тактические паттерны DDD — Entity, Value Object, Aggregate, Domain Event: как строить модель внутри Bounded Context.
- Интеграционные паттерны DDD — ACL, OHS, Shared Kernel и другие способы связать контексты.