Когда проект маленький — один разработчик держит всё в голове. Но как только команда вырастает и система усложняется, начинается путаница: одни называют сущность «заявкой», другие — «запросом», третьи — «ордером». Один модуль тянет данные из другого напрямую, и никто толком не знает, где заканчивается «продажи» и начинается «склад».
Стратегические паттерны DDD — это инструменты для наведения порядка на уровне всей системы: как разделить её на части, как назвать вещи своими именами и как организовать взаимодействие между частями.
Единый язык (Ubiquitous Language)
Представьте: бизнес говорит «платёжный спор», аналитик пишет «претензия», а в коде — PaymentProblem, Claim и Dispute вперемешку. Каждый «перевод» — потенциальная ошибка. Разработчик реализует не то, что задумал бизнес, потому что неверно понял термин.
Ubiquitous Language — общий словарь терминов, который используют и бизнес, и разработчики во всём: в разговорах, документации и коде.
Договорились называть «платёжный спор» словом Dispute — и это слово появляется везде:
# Плохо: термин взят из головы разработчика
class PaymentProblem:
def resolve(self) -> None: ...
# Хорошо: термин из бизнес-словаря
class Dispute:
def __init__(self, status: DisputeStatus) -> None:
self._status = status
def accept(self) -> None:
self._status = DisputeStatus.ACCEPTED
def reject(self, reason: str) -> None:
self._status = DisputeStatus.REJECTED
Когда код читается как описание бизнес-процесса — новые люди быстро входят в контекст, а на ревью сразу видно, если что-то не соответствует договорённостям.
Как формировать единый язык:
- составить глоссарий ключевых терминов вместе с бизнесом;
- если термин неоднозначен — выбрать один вариант и зафиксировать (в вики, ADR или README);
- на ревью проверять, что новый код использует слова из глоссария;
- обновлять глоссарий, когда появляются новые понятия.
Важный момент: единый язык действует только внутри одного контекста. В разных частях системы одно и то же слово может означать разное — и это нормально. Именно поэтому нужны Bounded Contexts.
Bounded Context — граница, внутри которой модель не врёт
Слово «клиент» в биллинге — это юридическое лицо с ИНН и адресом счёта. В службе поддержки — это пользователь с открытыми тикетами. В маркетинге — участник программы лояльности с баллами.
Если пытаться описать всё это одним классом Client, он обрастает полями из всех контекстов сразу:
# Антипаттерн: один класс для всех контекстов
class Client:
inn: str # нужно биллингу
billing_address: str # нужно биллингу
email: str # нужно поддержке
open_tickets: int # нужно поддержке
loyalty_level: str # нужно маркетингу
bonus_points: int # нужно маркетингу
Такой класс — «Бог-объект». Любое изменение в нём может сломать что-то неожиданное.
Bounded Context — явная граница, внутри которой модель и единый язык имеют чёткий, непротиворечивый смысл. За пределами этой границы то же слово может значить другое.
# Billing Context: Client — это плательщик
# billing/domain.py
class Client:
def __init__(
self,
id: ClientId,
inn: str,
billing_address: Address,
preferred_payment: PaymentMethod,
) -> None:
self._id = id
self._inn = inn
self._billing_address = billing_address
self._preferred_payment = preferred_payment
# Support Context: Client — это пользователь с тикетами
# support/domain.py
class Client:
def __init__(self, id: ClientId, email: str, open_tickets: list[Ticket]) -> None:
self._id = id
self._email = email
self._open_tickets = open_tickets
def is_vip(self) -> bool:
return any(t.priority is Priority.CRITICAL for t in self._open_tickets)
Два разных класса с одним именем — это не дублирование, а нормальный 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 и другие способы связать контексты.