Стратегические паттерны DDD

Стратегические паттерны DDD: Bounded Context, Context Map, Ubiquitous Language.

стратегические паттерны DDD

Все примеры в статье — на сквозном кейсе сайта: высоконагруженный маркетплейс.


Стратегические паттерны отвечают на вопрос — «как разделить большую систему на части и организовать взаимодействие между ними».

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

В этой статье рассматриваются все основные стратегические паттерны из книги Эрика Эванса «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 уведомления, фискализация чеков, антифрод-проверки, объектное хранилище для медиа товаров. Покупаем как сервис или подключаем готовое — внутри писать не имеет смысла.
diagram

4. Context Map (карта контекстов)

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

diagram

На этой схеме видно: 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 используется во всех частях системы
  • Любой модуль может напрямую обратиться к таблице любого другого модуля
  • Изменение в одном месте ломает непредсказуемые вещи в другом
  • Нет чётких границ ответственности между командами

Как исправлять

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

  1. Определить Core Domain — найти самую ценную часть системы
  2. Выделить Bounded Context для Core — провести границу, вынести в отдельный пакет/модуль
  3. Построить ACL — защитить новый чистый контекст от старого кода
  4. Повторить для следующего по важности контекста

Ключевая мысль: 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 — какие модули самые важные?