← Все статьи

DDD: предметно-ориентированное проектирование на практике

· 11 мин чтения

Поддомены, единый язык, ограниченные контексты и карта контекстов; сущности, объекты-значения, агрегаты и доменные события; Event Storming, роль аналитика, типичные ошибки и случаи, когда DDD не нужен.

Domain-Driven Design (DDD, предметно-ориентированное проектирование) — подход к разработке сложных систем, в котором структура программы повторяет устройство бизнеса, а разработчики и эксперты предметной области говорят на одном языке. Термин ввёл Эрик Эванс в книге 2003 года «Domain-Driven Design: Tackling Complexity in the Heart of Software». Двадцать с лишним лет спустя DDD — основной инструмент, которым режут систему на микросервисы, определяют границы команд и проектируют интеграции. Разберём, из чего состоит подход, как он применяется на практике и когда он не нужен.

Какую проблему решает DDD

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

  • одно слово означает разное в разных частях системы: «клиент» у продаж — потенциальный покупатель, у биллинга — плательщик по договору, у поддержки — пользователь, открывший обращение;
  • общая модель данных на всю компанию: таблица client на 150 колонок, которую боятся менять, потому что неизвестно, кто от неё зависит;
  • бизнес-правила размазаны по контроллерам, хранимым процедурам и интерфейсу, и никто не может ответить, где проверяется лимит;
  • требования аналитика и код разработчика описывают одно и то же разными словами, и при каждом изменении теряется часть смысла.

DDD предлагает две группы инструментов: стратегическое проектирование — как разделить большую предметную область на части и связать их, и тактическое проектирование — как выразить модель одной части в коде. Ценность в первую очередь в стратегической части; тактические шаблоны без неё дают мало.

Стратегическое проектирование

Предметная область и поддомены

Предметная область (domain) — то, чем занимается бизнес. Её делят на поддомены по степени важности для бизнеса:

  • основной (core) — то, чем компания отличается от конкурентов и на чём зарабатывает. Например, алгоритм ценообразования у агрегатора или маршрутизация у службы доставки. Сюда идут лучшие люди, здесь нужна глубокая модель и собственная разработка;
  • вспомогательный (supporting) — нужен бизнесу, но не даёт конкурентного преимущества: управление каталогом, внутренние справочники. Делается своими силами, но проще;
  • типовой (generic) — решён на рынке: аутентификация, бухгалтерия, рассылки, платежи. Покупается или берётся готовым.

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

Единый язык

Единый язык (ubiquitous language) — словарь терминов, который одинаково используют эксперты, аналитики, разработчики и тестировщики: в разговорах, требованиях, коде, тестах и интерфейсе. Если бизнес говорит «отгрузка», в коде должен быть Shipment, а не DeliveryRecord или OutboundOrder. Если в разговоре выясняется, что «отгрузка» бывает частичной, это сразу отражается и в модели.

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

Ограниченный контекст

Ограниченный контекст (bounded context) — граница, внутри которой модель и язык непротиворечивы. Это центральное понятие DDD. В интернет-магазине «товар» существует в нескольких контекстах:

КонтекстЧто такое «товар»Важные атрибуты
Каталогкарточка для витриныназвание, описание, фото, характеристики, категория
Ценообразованиепозиция прайс-листабазовая цена, скидки, правила акций
Складединица хранения (SKU)остаток, ячейка, габариты, вес, партия
Заказыстрока заказазафиксированная цена на момент покупки, количество
Доставкаместо в отправлениивес, габариты, хрупкость

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

Как найти границы контекстов:

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

Карта контекстов

Карта контекстов (context map) показывает, какие контексты есть и как они связаны. Связь описывается не только стрелкой «кто кого вызывает», но и типом отношений между командами — от этого зависит, кто под кого подстраивается:

  • Partnership (партнёрство) — две команды координируют изменения и релизы вместе;
  • Shared Kernel (общее ядро) — небольшая общая часть модели, которую меняют только по согласию обеих сторон;
  • Customer–Supplier (заказчик–поставщик) — нижестоящая команда влияет на приоритеты вышестоящей;
  • Conformist (конформист) — потребитель принимает модель поставщика как есть, потому что повлиять на неё не может;
  • Anticorruption Layer (предохранительный слой) — потребитель защищает свою модель переводчиком, который преобразует чужие понятия в свои. Обязателен при интеграции с унаследованными системами и внешними сервисами со сложной или неудачной моделью;
  • Open Host Service (служба с открытым протоколом) — поставщик публикует стабильный API для многих потребителей;
  • Published Language (общедоступный язык) — документированный формат обмена: схема событий, отраслевой стандарт, контракт OpenAPI или AsyncAPI;
  • Separate Ways (раздельное существование) — интеграция не стоит усилий, контексты решают задачу независимо.

Для аналитика карта контекстов — один из самых полезных артефактов: она объясняет, почему интеграция устроена именно так, где нужны преобразования данных и с кем согласовывать изменения.

Тактическое проектирование

Тактические шаблоны описывают, как выразить модель внутри одного контекста.

Сущность и объект-значение

Сущность (entity) — объект с идентичностью, которая сохраняется при изменении атрибутов: заказ, договор, клиент. Два заказа с одинаковым составом — всё равно разные заказы.

Объект-значение (value object) — объект, определяемый только своими атрибутами и неизменяемый: деньги (сумма и валюта), адрес, период, координаты. Две суммы «1500 RUB» взаимозаменяемы. Объекты-значения — недооценённый инструмент: они собирают проверки и поведение в одном месте. Вместо BigDecimal amount и String currency, разбросанных по коду, — тип Money, который не даст сложить рубли с долларами.

Агрегат

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

public class Order {                      // корень агрегата
    private final OrderId id;
    private final CustomerId customerId;  // ссылка на другой агрегат — только по идентификатору
    private final List<OrderLine> lines = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;

    public void addLine(ProductId product, int quantity, Money price) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Оформленный заказ нельзя изменить");
        }
        lines.add(new OrderLine(product, quantity, price));
    }

    public OrderPlaced place() {
        if (lines.isEmpty()) {
            throw new IllegalStateException("Пустой заказ нельзя оформить");
        }
        status = OrderStatus.PLACED;
        return new OrderPlaced(id, customerId, total());  // доменное событие
    }

    public Money total() {
        return lines.stream().map(OrderLine::amount).reduce(Money.ZERO, Money::add);
    }
}

Правила проектирования агрегатов, проверенные практикой:

  • агрегат охраняет инварианты — правила, которые должны выполняться всегда и сразу («оформленный заказ не меняется», «сумма строк равна итогу»);
  • агрегаты маленькие — в агрегат входит только то, что нужно для соблюдения инвариантов. Покупатель не входит в агрегат заказа, на него ссылаются по идентификатору;
  • одна транзакция — один агрегат. Если изменение должно затронуть несколько агрегатов, они согласуются через доменные события, асинхронно — это и есть согласованность в конечном счёте (eventual consistency);
  • агрегат сохраняется и загружается целиком.

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

Доменные события

Доменное событие — факт, который произошёл в предметной области и важен экспертам: «заказ оформлен», «платёж прошёл», «товар отгружен». События именуются в прошедшем времени, неизменяемы и несут данные, нужные подписчикам. Они связывают агрегаты внутри контекста и контексты между собой: заказ публикует OrderPlaced, склад резервирует товар, оплата выставляет счёт, уведомления отправляют письмо — и заказ ничего о них не знает.

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

Сервисы, репозитории, фабрики

  • Доменный сервис — операция предметной области, которая не принадлежит естественным образом ни одной сущности: расчёт стоимости доставки по тарифам перевозчика, перевод между счетами.
  • Прикладной сервис (application service) — сценарий использования: загрузить агрегат, вызвать его метод, сохранить, опубликовать события. Бизнес-правил в нём нет, он оркестрирует.
  • Репозиторий — коллекция агрегатов с точки зрения домена: «найти заказ», «сохранить заказ». Скрывает базу данных.
  • Фабрика — создание сложного агрегата в корректном состоянии.

Доменную модель обычно изолируют от инфраструктуры архитектурой «порты и адаптеры» (гексагональной) или «чистой архитектурой»: домен в центре не зависит от фреймворков, базы данных и брокеров, а они подключаются снаружи через интерфейсы.

Как это работает на практике

Совместное моделирование

DDD начинается не с кода, а с разговора с экспертами. Самые распространённые практики:

  • Event Storming (Альберто Брандолини) — участники выписывают на стикерах доменные события процесса в хронологическом порядке, затем добавляют команды, которые их вызывают, исполнителей, правила, внешние системы и проблемные места. За несколько часов становится видна вся картина процесса, спорные термины и естественные границы контекстов. Формат большого семинара подходит для исследования всей области, формат проектирования — для детальной модели одного процесса;
  • Domain Storytelling — эксперт рассказывает, как проходит работа, а модератор рисует историю пиктограммами: кто, что и с каким объектом делает;
  • Bounded Context Canvas — шаблон описания контекста: назначение, классификация поддомена, ключевые термины, входящие и исходящие взаимодействия, бизнес-решения.

Типовой путь проекта

  1. Исследовать предметную область на Event Storming с экспертами и командой разработки.
  2. Выделить поддомены и определить основной — туда направить основные силы.
  3. Наметить ограниченные контексты и нарисовать карту контекстов с типами отношений.
  4. Для каждого контекста составить глоссарий единого языка и зафиксировать его в документации и коде.
  5. Определить контракты между контекстами: API с OpenAPI для синхронных вызовов, события с AsyncAPI и схемами — для асинхронных.
  6. Внутри основного поддомена — проработать агрегаты, инварианты и события; во вспомогательных и типовых обойтись простыми решениями.
  7. Регулярно пересматривать модель: понимание предметной области растёт, и модель должна меняться вместе с ним.

Роль системного аналитика

Многое в DDD — работа, которую аналитик и так делает, только явно и системно:

  • глоссарий превращается в единый язык контекста и проверяется по коду и интерфейсу;
  • модель предметной области описывается в терминах агрегатов, сущностей, объектов-значений и инвариантов — это прямо ложится в код;
  • бизнес-правила формулируются как инварианты конкретного агрегата или как политики реакции на события;
  • карта контекстов и контракты — основа интеграционной архитектуры и постановок для смежных команд;
  • процессы в BPMN показывают, как события и команды разных контекстов складываются в сквозной бизнес-процесс.

Типичные ошибки

  • Только тактика без стратегии. Команда вводит агрегаты и репозитории, но не определяет контексты. Получается та же общая модель, только с дополнительными слоями.
  • Анемичная модель. Сущности — наборы полей с геттерами и сеттерами, вся логика в сервисах. Инварианты не охраняются, их проверка дублируется и расходится.
  • Огромные агрегаты. «Клиент со всеми заказами, договорами и платежами» — конфликты при параллельных изменениях, медленная загрузка, блокировки.
  • Общая база данных между контекстами. Границы контекстов формально есть, а на деле все читают и пишут одни таблицы — любое изменение схемы ломает соседей.
  • «DDD — это микросервисы». Контексты можно реализовать модулями одного приложения (модульный монолит) — часто это правильный первый шаг. Выделять сервисы стоит, когда границы проверены временем.
  • Единый язык на всю компанию. Попытка дать каждому термину одно определение для всех отделов противоречит самой идее ограниченных контекстов.
  • Сущность на каждую таблицу. Модель выводится из схемы базы данных, а не из поведения предметной области.

Когда DDD не нужен

DDD — инвестиция, и она окупается не везде:

  • простой CRUD — справочники, административные интерфейсы, формы без нетривиальных правил;
  • интеграционные и технические сервисы — конвертеры форматов, прокси, отчёты;
  • типовые поддомены, которые лучше купить или взять готовыми;
  • нет доступа к экспертам — без них единый язык и модель не построить, и подход вырождается в набор шаблонов кода.

При этом стратегические идеи полезны почти всегда: даже для небольшого проекта стоит понимать, где проходят границы смысла, и не смешивать в одной модели «клиента» продаж и «клиента» биллинга.

Что почитать

  • Эрик Эванс, «Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем» — первоисточник, «синяя книга».
  • Вон Вернон, «Реализация методов предметно-ориентированного проектирования» — практическое руководство, «красная книга»; его же «Предметно-ориентированное проектирование: самое основное» — краткое введение.
  • Влад Хононов, «Изучаем DDD — предметно-ориентированное проектирование» — современный и наиболее доступный обзор, включая связь с микросервисами и событийной архитектурой.
  • Альберто Брандолини, «Introducing EventStorming» — о совместном моделировании.

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

Карту контекстов удобно рисовать в PlantUML (в том числе диаграммами C4), доменные события описывать в AsyncAPI, а сквозные процессы — в редакторе BPMN.