Стратегические паттерны DDD
Стратегические паттерны DDD: Bounded Context, Context Map, Ubiquitous Language.
Все примеры в статье — на сквозном кейсе сайта: высоконагруженный маркетплейс.
Стратегические паттерны отвечают на вопрос — «как разделить большую систему на части и организовать взаимодействие между ними».
Когда система растёт, главная проблема — не качество кода внутри модуля, а границы между модулями: где заканчивается одна модель и начинается другая, кто кому что поставляет, как не допустить хаоса зависимостей. Стратегические паттерны помогают: определить границы ответственности команд и модулей, зафиксировать общий язык для каждой части системы, выбрать правильный способ интеграции между частями и сфокусировать ресурсы на том, что приносит бизнесу наибольшую ценность.
В этой статье рассматриваются все основные стратегические паттерны из книги Эрика Эванса «Domain-Driven Design».
Стратегические паттерны DDD: 1. Ubiquitous Language (единый язык)
Ubiquitous Language — это общий словарь терминов, который используют и бизнес, и разработчики. Термины из этого словаря должны совпадать в разговорах, документации и коде.
Зачем нужен
Типичная ситуация: бизнес говорит «заявка», аналитик пишет «запрос», а в коде — Request, Order, Application в разных местах. Каждый «перевод» термина — это потенциальная ошибка. Единый язык устраняет эту проблему: разработчики и бизнес понимают друг друга без «переводчика», код читается как описание бизнес-процесса, новые разработчики быстрее входят в контекст, код-ревью становится проще.
Как это выглядит в коде
Допустим, в бизнесе используют термины «спор по платежу» и «чарджбек». Команда договорилась использовать один термин — Dispute (спор).
// Плохо: в коде термин, которого нет в бизнес-словаре
class PaymentProblem {
void resolve() { /* ... */ }
}
// Хорошо: код отражает единый язык
class Dispute {
private final PaymentId paymentId;
private DisputeStatus status;
void accept() {
this.status = DisputeStatus.ACCEPTED;
}
void reject(String reason) {
this.status = DisputeStatus.REJECTED;
}
}
Как формировать единый язык
- Составить глоссарий ключевых терминов вместе с бизнесом
- Если термин неоднозначен — обсудить и выбрать один вариант
- Зафиксировать решение (вики, README, ADR)
- В код-ревью проверять соответствие кода глоссарию
- Пересматривать глоссарий при появлении новых понятий
Важно: единый язык действует внутри одного Bounded Context. В разных контекстах одно и то же бизнес-понятие может называться по-разному — и это нормально.
2. Bounded Context (ограниченный контекст)
Bounded Context — это явная граница, внутри которой определённая модель и единый язык имеют чёткий, непротиворечивый смысл.
Зачем нужен
Одно и то же слово в разных частях бизнеса часто означает разные вещи. Без явных границ эти значения смешиваются в коде, что приводит к «God Object» — классам, которые пытаются быть всем сразу.
// Антипаттерн: один класс для всех контекстов
class Client {
private String inn; // нужно биллингу
private String billingAddress; // нужно биллингу
private String email; // нужно поддержке
private int openTickets; // нужно поддержке
private String loyaltyLevel; // нужно маркетингу
private int bonusPoints; // нужно маркетингу
}
Этот класс будет расти бесконечно, а изменения для одного отдела будут ломать другие. Правильный подход — отдельная модель в каждом контексте:
// Billing Context
package com.example.billing.domain;
class Client {
private final ClientId id;
private final String inn;
private final Address billingAddress;
private PaymentMethod preferredPayment;
}
// Support Context
package com.example.support.domain;
class Client {
private final ClientId id;
private final String email;
private final List openTickets;
boolean isVip() {
return openTickets.stream()
.anyMatch(t -> t.priority() == Priority.CRITICAL);
}
}
Как определить границу контекста
Признаки того, что нужен отдельный контекст: разный язык — одни и те же слова значат разное для разных людей; разные бизнес-правила — логика валидации и поведение отличаются; разные жизненные циклы — одна часть меняется часто, другая стабильна; разные команды — над разными частями работают разные люди.
Bounded Context ≠ микросервис
Bounded Context — это логическая граница модели. Микросервис — физическая граница деплоя. Они могут совпадать, но не обязаны: один микросервис может содержать несколько контекстов (на старте проекта), один контекст может быть разделён на несколько микросервисов (для масштабирования), в монолите контексты разделяются через пакеты/модули.
3. Поддомены: Core / Supporting / Generic
Не всё в системе одинаково важно для бизнеса. Поддомены классифицируют части системы по ценности, чтобы правильно распределить ресурсы.
Core Domain — то, что приносит бизнесу конкурентное преимущество. Здесь оправдана сложная доменная модель и DDD по полной.
Supporting Subdomain — важно для работы бизнеса, но не даёт уникального преимущества. Можно использовать упрощённые подходы (CRUD + валидация).
Generic Subdomain — типовые функции (авторизация, уведомления, логирование). Идеально для покупных решений или open-source.
Пример для нашего сквозного кейса — маркетплейса:
- Core: каталог и ранжирование, оформление заказа, расчёты с продавцами, ценообразование с учётом промо. Здесь зарабатываем — и здесь нужны сильная доменная модель, инварианты, аудит.
- Supporting: личный кабинет покупателя, кабинет продавца, отзывы и рейтинги, споры. Нужно, но не даёт уникального преимущества — упрощённые модели + CRUD достаточно.
- Generic: SMS/e-mail уведомления, фискализация чеков, антифрод-проверки, объектное хранилище для медиа товаров. Покупаем как сервис или подключаем готовое — внутри писать не имеет смысла.
4. Context Map (карта контекстов)
Context Map — это визуальная схема всех Bounded Contexts в системе и связей между ними. Это не диаграмма классов и не архитектурная схема — это карта ответственности и зависимостей. Она помогает увидеть полную картину системы, зафиксировать тип интеграции между контекстами, обнаружить проблемные связи и помочь новым людям быстро понять архитектуру.
На этой схеме видно: Orders предоставляет данные для Payments через Open Host Service, Payments защищается от внешнего платёжного шлюза через Anti-Corruption Layer, Accounting принимает модель Payments как есть (Conformist), Shipping получает данные от Orders по модели Customer–Supplier.
5. Strategic Distillation (стратегическая дистилляция)
Дистилляция — это процесс выделения самого важного в домене. Цель — определить, где находится настоящая бизнес-ценность, и сфокусировать на ней максимум ресурсов.
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 — лучшие разработчики, максимальное покрытие тестами. Supporting — пишем проще, меньше абстракций. Generic — покупаем готовое.
6. Big Ball of Mud (антипаттерн)
Big Ball of Mud — это система, в которой нет явных границ, модели перемешаны, а зависимости хаотичны. По сути — отсутствие стратегического дизайна.
Как выглядит
- Один класс
Client/User/Orderиспользуется во всех частях системы - Любой модуль может напрямую обратиться к таблице любого другого модуля
- Изменение в одном месте ломает непредсказуемые вещи в другом
- Нет чётких границ ответственности между командами
Как исправлять
Полностью переписать систему обычно невозможно. Практичный подход — постепенная декомпозиция:
- Определить Core Domain — найти самую ценную часть системы
- Выделить Bounded Context для Core — провести границу, вынести в отдельный пакет/модуль
- Построить ACL — защитить новый чистый контекст от старого кода
- Повторить для следующего по важности контекста
Ключевая мысль: Big Ball of Mud — это не приговор. Это отправная точка, из которой можно постепенно выделять чистые контексты, начиная с самого ценного.
Чеклист: стратегическое проектирование
Язык и границы:
- Выписать ключевые термины бизнеса (глоссарий)
- Проверить: одинаково ли понимается термин в разных частях системы?
- Если смысл различается — выделить отдельные Bounded Contexts
- Зафиксировать единый язык для каждого контекста
Классификация:
- Определить Core Domain — что даёт конкурентное преимущество?
- Определить Supporting — что важно, но не уникально?
- Определить Generic — что можно купить или взять готовое?
Интеграции:
- Нарисовать Context Map — какие контексты есть и как они связаны?
- Для каждой связи выбрать паттерн интеграции
- Для внешних систем — обязательно ACL
- Для API с несколькими потребителями — OHS + Published Language
Эволюция:
- Есть ли признаки Big Ball of Mud? Где?
- Составить Domain Vision Statement для Core Domain
- Определить Highlighted Core — какие модули самые важные?