DDD — это не про код. Это про то, как вы думаете о системе до того, как напишете первую строку.
Если вы когда-нибудь гуглили «что такое DDD» и через 10 минут чтения чувствовали себя глупее, чем до — вы не одиноки. Большинство статей о Domain-Driven Design написаны людьми, которые уже всё поняли, и для людей, которые уже почти всё поняли. Остальным — удачи.
Эта статья для тех, кто слышал слова «bounded context», «aggregate» и «ubiquitous language», кивал на созвоне, а потом тихо искал в Google, что это значит. Мы разберём три ключевых концепции DDD так, чтобы после прочтения вы могли не просто кивать, а аргументированно спорить на архитектурных ревью.
Если вы хотите освежить общее понимание подхода, загляните в обзорную статью о DDD — там мы разбирали, кому и зачем это нужно. Здесь пойдём глубже.
Ubiquitous Language: единый язык, без которого всё разваливается
Давайте начнём с концепции, которую чаще всего недооценивают. Ubiquitous Language (единый язык) — это не глоссарий, который написали в Confluence и забыли. Это соглашение между бизнесом и разработкой о том, как называть вещи — и в разговоре, и в коде, и в документации.
Ubiquitous Language — это когда бизнес-аналитик говорит «полис», тимлид говорит «полис», разработчик пишет в коде
Policy, и тестировщик проверяет именноPolicy. НеDocument, неContract, неInsuranceItem.
Почему это так важно
Представьте страховую компанию. Менеджер продукта говорит «клиент». Разработчик в коде пишет User. Аналитик в требованиях пишет «застрахованное лицо». Тестировщик в тест-кейсах называет это «пользователь». Все говорят об одном и том же — но каждый по-своему.
Через полгода появляется новая фича: «клиент может быть юридическим лицом». И начинается хаос. В одном месте User расширяют, в другом создают CompanyUser, в третьем — LegalEntity. А менеджер продукта всё ещё думает, что «клиент» — это просто клиент.
С единым языком эта проблема решается на старте. Команда договаривается: в контексте оформления полиса «клиент» = Policyholder (страхователь). В контексте выплат — Claimant (заявитель). Это разные роли, и они имеют право быть разными сущностями.
Как внедрить Ubiquitous Language на практике
Это не разовое мероприятие, а непрерывный процесс:
| Что делать | Как именно |
|---|---|
| Создать глоссарий домена | Markdown-файл в репозитории (не Confluence — он умрёт раньше проекта). Формат: термин → определение → пример в коде |
| Называть классы и методы на языке бизнеса | Не processData(), а calculatePremium(). Не handleEvent(), а approveClaimPayment() |
| Проводить Event Storming | Воркшоп, где бизнес и разработка вместе выписывают события домена на стикерах. Лучший способ синхронизировать язык |
| Ревьюить код на соответствие языку | Если в коде User, а в домене «Страхователь» — это баг, даже если тесты зелёные |
Bounded Context: границы, которые спасают от хаоса
Bounded Context (ограниченный контекст) — это, пожалуй, самая важная и самая неправильно понимаемая концепция DDD. Многие думают, что bounded context = микросервис. Это не так (хотя связь есть, и мы до неё дойдём).
Bounded Context — это граница, внутри которой определённая модель домена имеет единственное, непротиворечивое значение. За пределами этой границы та же модель может значить что-то совсем другое.
Аналогия: одно слово — разные значения
Слово «счёт» в банке означает одно, в ресторане — совсем другое, а в футболе — третье. Если вы попросите «выставить счёт» — результат зависит от контекста. Bounded Context — это и есть этот контекст. Он определяет, что именно означает каждый термин внутри себя.
В интернет-магазине «товар» в контексте каталога — это карточка с описанием, фотографиями и характеристиками. «Товар» в контексте склада — это физическая единица с весом, габаритами и адресом ячейки хранения. «Товар» в контексте доставки — это строчка в накладной с идентификатором и количеством. Три разных bounded context — три разных модели для одного и того же физического объекта.
Диаграмма показывает, как один и тот же «товар» превращается в четыре разные модели в зависимости от контекста. Обратите внимание на пунктирные линии — это связи между контекстами. Они идут через идентификаторы (ProductId, SKU), а не через общие объекты. Каждый контекст владеет своей моделью и не зависит от внутренностей другого.
Как определить границы контекстов
Вот несколько сигналов, что пора провести границу:
- Одно слово означает разное. Если «заказ» для отдела продаж и для склада — это разные вещи с разными атрибутами, у вас два контекста.
- Разные команды работают с разными частями системы. Команда каталога и команда логистики не должны менять одни и те же таблицы в базе данных.
- Разная частота изменений. Каталог обновляется каждый день, а биллинг-модуль не трогают месяцами. Значит, им не надо жить в одном коде.
- Разные правила согласованности. Каталог может быть eventually consistent (обновил — отобразится через 5 секунд), а биллинг требует strict consistency (списание должно быть атомарным).
Bounded Context и модульный монолит
Bounded context — это логическая граница. Она не требует отдельного сервиса или отдельной базы данных. Вы можете реализовать несколько bounded contexts внутри модульного монолита — и это будет абсолютно правильный подход. Модуль = bounded context, только всё в одном деплое.
Переход от модульного монолита к микросервисам — это, по сути, вынесение bounded contexts в отдельные процессы. Если границы проведены правильно, эта миграция проходит относительно безболезненно (относительно — ключевое слово).
Aggregate: единица консистентности, которая не даёт данным развалиться
Aggregate (агрегат) — это кластер связанных объектов, которые мы рассматриваем как единое целое с точки зрения изменения данных. У агрегата есть корневой объект (Aggregate Root), через который происходит всё взаимодействие с внешним миром.
Агрегат — это коробка с инструкцией. Вы можете взять коробку целиком, можете положить обратно, но не можете вытащить одну деталь и изменить её в обход коробки. Всё проходит через корень.
Зачем нужен агрегат
Представьте интернет-магазин. У вас есть заказ (Order), в нём — позиции (OrderLine), у каждой позиции — ссылка на товар и количество. Если два запроса одновременно пытаются изменить один и тот же заказ — например, клиент добавляет товар, а менеджер меняет скидку — то без агрегата вы рискуете получить несогласованные данные. Агрегат гарантирует, что все изменения внутри него проходят атомарно и согласованно.
На диаграмме — агрегат «Заказ». Корень агрегата (Order) — единственная точка входа. Позиции заказа, адрес доставки и скидка живут внутри агрегата и не могут быть изменены извне напрямую. Серые блоки — внешние агрегаты и контексты. Обратите внимание: связь с ними идёт через идентификаторы, а не через прямые ссылки на объекты.
Правила проектирования агрегатов
Вот основные принципы, которые помогут не наступить на грабли:
1. Маленький агрегат — лучше большого.
Начинающие любят пихать в один агрегат всё подряд: заказ + клиент + товары + отзывы + историю доставки. Получается монстр, который блокирует половину базы данных при каждом изменении. Правило: агрегат должен быть минимально достаточным для обеспечения одного бизнес-инварианта.
2. Ссылки по идентификатору, а не по объекту.
Агрегат «Заказ» хранит customerId, а не объект Customer целиком. Это позволяет загружать и сохранять агрегат независимо. Если вам нужен полный объект клиента — обратитесь в соответствующий сервис.
3. Один агрегат — одна транзакция.
Изменения в одном агрегате сохраняются в одной транзакции. Если нужно изменить два агрегата — это две транзакции, и между ними eventual consistency. Да, это менее «удобно», чем один гигантский BEGIN...COMMIT, но зато масштабируется.
4. Бизнес-правила живут внутри.
Агрегат сам проверяет свои инварианты. «Заказ не может иметь отрицательную сумму», «нельзя добавить больше 50 позиций», «скидка не может превышать 30%» — всё это логика агрегата, а не внешнего сервиса.
Агрегат vs Entity vs Value Object
Чтобы окончательно разложить всё по полочкам:
| Концепция | Что это | Пример | Идентичность |
|---|---|---|---|
| Entity (сущность) | Объект с уникальной идентичностью, важен «кто это» | Заказ #12345, Клиент Иванов | Определяется по id |
| Value Object (объект-значение) | Объект без идентичности, важно «что в нём» | Адрес (город, улица, дом), Деньги (100 руб.) | Определяется по содержимому |
| Aggregate (агрегат) | Кластер сущностей и объектов-значений с единым корнем | Заказ + позиции + адрес доставки | Определяется по корню |
Адрес доставки — value object. Если два заказа отправляются по одному адресу, это два одинаковых value object, а не ссылка на один и тот же объект. Замените улицу — и вы создали новый value object, а не изменили старый.
Как три концепции работают вместе: пример из e-commerce
Давайте соберём всё в одну картину. Представьте, что мы проектируем систему для маркетплейса.
Диаграмма показывает полную картину: ubiquitous language пронизывает все контексты (пунктирные линии), bounded contexts определяют границы (рамки), а агрегаты — единицы консистентности внутри каждого контекста (блоки). Связь между контекстами — через идентификаторы и доменные события (OrderPlaced, OrderConfirmed), а не через прямые вызовы.
Что это даёт на практике
- Команда каталога может менять структуру карточки товара, и это не ломает заказы. Потому что контекст заказов знает о товаре только
productId. - Команда платежей может добавить новый платёжный метод, и это не требует изменений в коде заказов. Потому что связь — через событие
OrderPlaced. - Новый разработчик читает глоссарий и понимает, что
Claimantв контексте возвратов — это не тот жеBuyer, хотя физически это один человек.
Типичные ошибки при внедрении DDD
Ошибка 1: «Давайте сразу весь проект на DDD»
DDD — это не всё или ничего. Эрик Эванс (автор оригинальной книги) сам говорит: применяйте DDD к Core Domain — к самой важной и сложной части системы. CRUD-админка для управления справочниками не нуждается в агрегатах и bounded contexts. Это нормально.
Ошибка 2: Bounded Context = микросервис
Bounded context — это логическая граница. Микросервис — это физическая граница (отдельный процесс, деплой, база). Один bounded context может жить как модуль в модульном монолите. А может стать микросервисом. Или даже несколькими микросервисами. Не путайте карту и территорию.
Ошибка 3: Гигантские агрегаты
«А давайте Order будет содержать Customer, который содержит Address, который содержит City, который содержит Country…» — и вот у вас агрегат, который загружает полбазы данных. Держите агрегаты маленькими. Если сомневаетесь — разделяйте.
Ошибка 4: Забыть про Ubiquitous Language
Многие начинают с bounded contexts и агрегатов (потому что это звучит «технично»), но пропускают единый язык. В итоге в коде Order, в документации «заявка», на созвоне «тикет», а в базе — request. Четыре названия для одной сущности. DDD без ubiquitous language — это как навигатор без карты: механизм есть, а маршрута нет.
Когда DDD не нужен (и это нормально)
Это не серебряная пуля. DDD избыточен, если:
- Домен простой. Приложение для учёта задач, блог, лендинг — здесь CRUD достаточно. Не нужно моделировать bounded contexts для формы обратной связи.
- Команда из 1–2 человек. DDD требует коммуникации между бизнесом и разработкой. Если вы сам себе аналитик, разработчик и тестировщик — ubiquitous language формируется автоматически (в вашей голове).
- Прототип или MVP. Когда главное — проверить гипотезу, а не построить идеальную архитектуру. «Работает — не трогай» на этом этапе — вполне разумная стратегия.
Но если ваш проект растёт, домен усложняется, а команда расширяется — именно в этот момент отсутствие DDD начинает больно кусать. И чем позже вы начнёте, тем дороже обойдётся рефакторинг.
Что почитать дальше
Если тема зацепила, вот осмысленный порядок погружения:
- Эрик Эванс — «Domain-Driven Design: Tackling Complexity in the Heart of Software» — та самая «синяя книга». Тяжёлая, но фундаментальная.
- Вон Вернон — «Implementing Domain-Driven Design» — «красная книга». Более практичная, с примерами на Java.
- Вон Вернон — «Domain-Driven Design Distilled» — если 600 страниц страшно. Квинтэссенция за 150 страниц.
- Event Storming (Альберто Брандолини) — метод обнаружения bounded contexts через совместные воркшопы. Начните с бесплатных материалов на eventstorming.com.
Заключение
DDD — это три простых идеи в непростой обёртке. Единый язык — чтобы все говорили одинаково. Bounded contexts — чтобы каждая часть системы знала свои границы. Агрегаты — чтобы данные не расползались как тесто без формы.
Ни одна из этих идей не требует специального фреймворка, дорогого инструмента или трёхмесячного обучения. Они требуют только одного — привычки думать о системе в терминах бизнес-домена, а не в терминах таблиц базы данных.
Начните с малого: договоритесь с командой о десяти ключевых терминах вашего домена. Запишите их. Убедитесь, что код использует те же названия. Поздравляю — вы уже практикуете DDD.
P.S. Когда на следующем собеседовании вас спросят «что такое DDD?», не отвечайте определением из Википедии. Просто скажите: «Это когда код назван так же, как бизнес-процессы, бизнес-процессы разделены на контексты, а данные не меняются в обход правил». И наблюдайте, как интервьюер одобрительно кивает — потому что он тоже только что это загуглил.