← назад к разделу

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

Стратегические паттерны 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 уведомления, проверки на мошенничество, хранилище медиафайлов товаров. Покупаем как сервис или подключаем готовое.
diagram

Context Map — карта зависимостей между командами

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

diagram

На этой карте видно:

  • 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 используется во всей системе;
  • нет чётких границ ответственности между командами;
  • никто не знает, какой эффект у изменения.

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

  1. Найти Core Domain — самую ценную часть.
  2. Выделить для неё Bounded Context — вынести в отдельный пакет или модуль.
  3. Поставить Anti-Corruption Layer — защитить новый чистый контекст от остального кода.
  4. Повторить для следующего по важности контекста.

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 и другие способы связать контексты.