Когда проект маленький — один разработчик держит всё в голове. Но как только команда вырастает и система усложняется, начинается путаница: одни называют сущность «заявкой», другие — «запросом», третьи — «ордером». Один модуль тянет данные из другого напрямую, и никто толком не знает, где заканчивается «продажи» и начинается «склад».
Стратегические паттерны 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;
}
// Support Context: Client — это пользователь с тикетами
package com.example.support.domain;
class Client {
private final ClientId id;
private final String email;
private final List<Ticket> openTickets;
boolean isVip() {
return openTickets.stream()
.anyMatch(t -> t.priority() == Priority.CRITICAL);
}
}
Два разных класса с одним именем — это не дублирование, а нормальный DDD. Каждый класс несёт только те поля, которые важны в его контексте.
Как понять, что нужна граница:
- разные команды имеют в виду разное, говоря одно слово;
- одна часть системы меняется часто, другая стабильна;
- над разными частями работают разные команды.
Bounded Context — это не микросервис. Это логическая граница модели, а микросервис — физическая граница деплоя. На старте проекта один микросервис может содержать несколько контекстов, разделённых пакетами.
Поддомены: где вкладывать силы
Не все части системы одинаково важны. DDD делит предметную область на три типа поддоменов — чтобы команда правильно распределяла усилия.
Core Domain — то, что даёт бизнесу конкурентное преимущество. Здесь зарабатывают деньги. Именно сюда нужна самая сложная и тщательная доменная модель, лучшие разработчики, максимальное покрытие тестами.
Supporting Subdomain — важно для работы, но не уникально. Без него не обойтись, но конкуренты делают то же самое. Можно применять более простые подходы: CRUD с валидацией достаточно.
Generic Subdomain — типовые задачи: авторизация, уведомления, выставление счетов, хранение файлов. Обычно выгоднее купить готовое решение или подключить открытую библиотеку, чем писать самостоятельно.
Пример для маркетплейса:
- Core: каталог и ранжирование, оформление заказа, расчёты с продавцами, динамическое ценообразование. Здесь нужна сильная доменная модель с инвариантами.
- Supporting: личный кабинет покупателя, кабинет продавца, отзывы и рейтинги, споры по заказам. Нужны, но упрощённых моделей достаточно.
- Generic: SMS/e-mail уведомления, проверки на мошенничество, хранилище медиафайлов товаров. Покупаем как сервис или подключаем готовое.
Context Map — карта зависимостей между командами
Когда контексты определены, нужно зафиксировать, как они взаимодействуют. Context Map — это схема всех Bounded Contexts и связей между ними. Не диаграмма классов, а карта ответственности: кто кому поставляет данные и на каких условиях.
На этой карте видно:
- Orders предоставляет данные Payments через Open Host Service — публичный API со стабильным контрактом.
- Payments защищается от внешнего платёжного шлюза через Anti-Corruption Layer — переводит чужую модель в свою.
- Accounting принимает модель Payments как есть (Conformist) — адаптируется под поставщика.
- Shipping получает данные от Orders по условию Customer–Supplier — Orders учитывает потребности Shipping при изменениях.
Карта помогает обнаружить проблемные зависимости до того, как они попадут в код.
Стратегическая дистилляция
Дистилляция — процесс выделения самого важного в домене. Цель: понять, где настоящая бизнес-ценность, и направить туда усилия.
Без дистилляции команда тратит одинаково много времени на модуль рассылок и на ключевой алгоритм ранжирования. После дистилляции приоритеты становятся явными.
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/
Big Ball of Mud — что происходит без стратегии
Big Ball of Mud — система без явных границ: модели перемешаны, любой модуль обращается к таблицам другого напрямую, изменение в одном месте ломает непредсказуемые вещи в другом.
Признаки:
- один класс
UserилиOrderиспользуется во всей системе; - нет чётких границ ответственности между командами;
- никто не знает, какой эффект у изменения.
Полностью переписать такую систему обычно невозможно. Практичный путь — постепенная декомпозиция:
- Найти Core Domain — самую ценную часть.
- Выделить для неё Bounded Context — вынести в отдельный пакет или модуль.
- Поставить Anti-Corruption Layer — защитить новый чистый контекст от остального кода.
- Повторить для следующего по важности контекста.
Big Ball of Mud — не приговор, а отправная точка.
Коротко
- Ubiquitous Language — общий словарь терминов для бизнеса и разработчиков. Один термин — одно слово везде: в разговорах, документации и коде.
- Bounded Context — граница, внутри которой модель однозначна. За пределами границы то же слово может значить другое.
- Bounded Context — логическая граница, а не физическая. Один сервис может содержать несколько контекстов.
- Core Domain — где бизнес зарабатывает, здесь сложная модель и лучшие ресурсы. Supporting — нужно, но без уникальных требований. Generic — покупаем или берём готовое.
- Context Map — карта всех контекстов и типов связей между ними. Помогает увидеть проблемные зависимости.
- Стратегическая дистилляция — осознанное решение, куда вкладывать максимум усилий.
- Big Ball of Mud исправляют постепенно: начиная с Core Domain и выстраивая Anti-Corruption Layer вокруг нового чистого контекста.
Что почитать дальше
- Что такое DDD и зачем он нужен — если не читали: с чего начинается доменно-ориентированное проектирование.
- Тактические паттерны DDD — Entity, Value Object, Aggregate, Domain Event: как строить модель внутри Bounded Context.
- Интеграционные паттерны DDD — ACL, OHS, Shared Kernel и другие способы связать контексты.