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 — шаблон описания контекста: назначение, классификация поддомена, ключевые термины, входящие и исходящие взаимодействия, бизнес-решения.
Типовой путь проекта
- Исследовать предметную область на Event Storming с экспертами и командой разработки.
- Выделить поддомены и определить основной — туда направить основные силы.
- Наметить ограниченные контексты и нарисовать карту контекстов с типами отношений.
- Для каждого контекста составить глоссарий единого языка и зафиксировать его в документации и коде.
- Определить контракты между контекстами: API с OpenAPI для синхронных вызовов, события с AsyncAPI и схемами — для асинхронных.
- Внутри основного поддомена — проработать агрегаты, инварианты и события; во вспомогательных и типовых обойтись простыми решениями.
- Регулярно пересматривать модель: понимание предметной области растёт, и модель должна меняться вместе с ним.
Роль системного аналитика
Многое в DDD — работа, которую аналитик и так делает, только явно и системно:
- глоссарий превращается в единый язык контекста и проверяется по коду и интерфейсу;
- модель предметной области описывается в терминах агрегатов, сущностей, объектов-значений и инвариантов — это прямо ложится в код;
- бизнес-правила формулируются как инварианты конкретного агрегата или как политики реакции на события;
- карта контекстов и контракты — основа интеграционной архитектуры и постановок для смежных команд;
- процессы в BPMN показывают, как события и команды разных контекстов складываются в сквозной бизнес-процесс.
Типичные ошибки
- Только тактика без стратегии. Команда вводит агрегаты и репозитории, но не определяет контексты. Получается та же общая модель, только с дополнительными слоями.
- Анемичная модель. Сущности — наборы полей с геттерами и сеттерами, вся логика в сервисах. Инварианты не охраняются, их проверка дублируется и расходится.
- Огромные агрегаты. «Клиент со всеми заказами, договорами и платежами» — конфликты при параллельных изменениях, медленная загрузка, блокировки.
- Общая база данных между контекстами. Границы контекстов формально есть, а на деле все читают и пишут одни таблицы — любое изменение схемы ломает соседей.
- «DDD — это микросервисы». Контексты можно реализовать модулями одного приложения (модульный монолит) — часто это правильный первый шаг. Выделять сервисы стоит, когда границы проверены временем.
- Единый язык на всю компанию. Попытка дать каждому термину одно определение для всех отделов противоречит самой идее ограниченных контекстов.
- Сущность на каждую таблицу. Модель выводится из схемы базы данных, а не из поведения предметной области.
Когда DDD не нужен
DDD — инвестиция, и она окупается не везде:
- простой CRUD — справочники, административные интерфейсы, формы без нетривиальных правил;
- интеграционные и технические сервисы — конвертеры форматов, прокси, отчёты;
- типовые поддомены, которые лучше купить или взять готовыми;
- нет доступа к экспертам — без них единый язык и модель не построить, и подход вырождается в набор шаблонов кода.
При этом стратегические идеи полезны почти всегда: даже для небольшого проекта стоит понимать, где проходят границы смысла, и не смешивать в одной модели «клиента» продаж и «клиента» биллинга.
Что почитать
- Эрик Эванс, «Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем» — первоисточник, «синяя книга».
- Вон Вернон, «Реализация методов предметно-ориентированного проектирования» — практическое руководство, «красная книга»; его же «Предметно-ориентированное проектирование: самое основное» — краткое введение.
- Влад Хононов, «Изучаем DDD — предметно-ориентированное проектирование» — современный и наиболее доступный обзор, включая связь с микросервисами и событийной архитектурой.
- Альберто Брандолини, «Introducing EventStorming» — о совместном моделировании.
DDD — не фреймворк и не архитектурный шаблон, а способ мышления: сначала понять предметную область и её естественные границы, затем отразить их в языке, коде и интеграциях. Чем сложнее бизнес, тем дороже обходится система, устроенная иначе, чем сам бизнес.
Карту контекстов удобно рисовать в PlantUML (в том числе диаграммами C4), доменные события описывать в AsyncAPI, а сквозные процессы — в редакторе BPMN.
