Domain-Driven Design (DDD)
Общее
Domain-Driven Design (DDD), предметно-ориентированное проектирование — подход к разработке программного обеспечения, в котором первичным источником проектных решений выступает предметная область (domain): бизнес, ради автоматизации которого существует система. Подход сформулирован Эриком Эвансом (Eric Evans) в книге Domain-Driven Design: Tackling Complexity in the Heart of Software (Addison-Wesley, 2003) — подзаголовок «борьба со сложностью в сердце ПО» передаёт главную мысль: самая опасная сложность корпоративных систем лежит не в технологиях, а в самой предметной области, и именно её нужно научиться укрощать проектированием.
Философия DDD умещается в одно правило: код должен говорить на языке домена. Если эксперт называет процедуру «приостановкой обслуживания», в коде должен быть метод suspendService, а не updateStatus(flag=3); если бизнес различает «списание» и «подтверждение списания», это различие обязано существовать и в модели. Язык, на котором эксперты описывают бизнес, и язык, на котором написан код, — один и тот же. Тогда код перестаёт быть «техническим переводом» требований и становится исполнимой моделью бизнеса: читая его, разработчик изучает сам домен, а расхождение между разговорами экспертов и кодом трактуется как дефект и устраняется.
Второй столп философии: модель — это не диаграмма и не схема базы данных. Модель в смысле DDD — система понятий, выработанная совместно разработчиками и экспертами и воплощённая в коде; диаграммы и таблицы — лишь её проекции. Отсюда характерный для DDD стиль работы: моделирование — непрерывный процесс уточнения, а не разовая фаза проектирования перед кодированием. Первая модель всегда наивна; ценность приносят итерации «углубился в домен — уточнил модель — переписал код».
Книга Эванса дала словарь и философию, но многие её идеи ждали операционального воплощения ещё десять лет. Его дал Вон Вернон (Vaughn Vernon) в книге Implementing Domain-Driven Design (2013): стратегическое проектирование доведено до практических рецептов (карты контекстов, шаблоны интеграции), а тактические паттерны — до правил проектирования агрегатов, которые цитируют до сих пор. Позднее Эванс выпустил краткий справочник Domain-Driven Design Reference: Definitions and Pattern Summaries (2015) — выжимку определений без нарратива, удобную для ежедневного обращения.
| Год | Событие | Значение для подхода |
|---|---|---|
| 2003 | Эрик Эванс, Domain-Driven Design | Первоисточник: философия, словарь, стратегические и тактические паттерны |
| 2010 | Вернон, серия статей Effective Aggregate Design | Практические правила проектирования агрегатов |
| 2011 | Альберто Брандолини, Event Storming | Быстрое совместное обнаружение модели и границ |
| 2013 | Вернон, Implementing Domain-Driven Design | Операциональное воплощение: рецепты, шаблоны интеграции |
| 2015 | Эванс, Domain-Driven Design Reference | Сводка определений — «карманный» референс |
| 2010-е | Волна микросервисов | Bounded context как метод проведения границ сервисов |
Исторически DDD вырос из объектного моделирования 1990-х: анализа паттернов Мартина Фаулера (Analysis Patterns, 1997), проектирования по ответственности Ребекки Вирфс-Брок (Responsibility-Driven Design), идей предметно-ориентированных языков. Эванс не изобрёл объектное моделирование — он собрал разрозненные приёмы в связную методологию с собственным словарём и показал, что моделирование — центральная инженерная дисциплина, а не пролог к кодированию. Вторая волна популярности пришла в 2010-х вместе с микросервисами: выяснилось, что стратегический DDD даёт осмысленный метод проведения границ сервисов, а Event Storming сделал совместное моделирование быстрым и наглядным.
Полезно сразу зафиксировать, чем DDD не является. Это не технология и не библиотека — паттерны DDD выражаются на любом объектном языке и вообще без объектов. Это не процесс разработки — DDD не предписывает спринты, церемонии или порядок релизов; он сочетается и с agile, и с классическим процессом. Наконец, это не серебряная пуля и не набор GoF-паттернов «про домен»: репозиторий из DDD и синглтон из GoF живут в разных мирах — первые описывают структуру модели, вторые — механику классов. DDD — метод проектирования, применимый при определённых условиях (о них — раздел «Когда DDD избыточен»).
DDD принято делить на два уровня, и это разделение принципиально. Стратегическое проектирование отвечает на вопрос «из каких частей состоит система и как они связаны»: единый язык, ограниченные контексты, карта контекстов, поддомены. Тактическое проектирование отвечает на вопрос «как моделировать внутри одного контекста»: сущности, объекты-значения, агрегаты, репозитории, доменные события. Распространённая ошибка — начинать с тактики (завести агрегаты и репозитории) без стратегической работы (провести границы контекстов). Эванс и Вернон подчёркивают обратный порядок: тактика без стратегии бесполезна, а стратегия без тактики уже приносит значительную часть пользы.
В этой статье DDD рассматривается с инженерной, архитектурной стороны. Управленческий разбор подхода — влияние на команды, планирование и инвестиции — вынесен в материал «DDD — Domain Driven Design» из серии driven-подходов. Порядок чтения ниже повторяет порядок внедрения: сначала стратегия (границы и язык), затем тактика (строительные блоки), затем правила агрегатов, связи с микросервисами и Clean Architecture и, наконец, честные границы применимости.
Стратегическое проектирование
Стратегический уровень оперирует четырьмя понятиями: единый язык (Ubiquitous Language), ограниченный контекст (Bounded Context), карта контекстов (Context Map) и поддомен (Subdomain). Вместе они отвечают на самый дорогой вопрос архитектуры: где проходят границы системы и её частей.
Единый язык — общий словарь разработчиков, аналитиков и экспертов, закреплённый в коде. Язык не изобретается за столом, а выращивается в разговорах о домене и обязан быть строгим: каждое понятие имеет одно имя, у каждого имени — одно значение. Неточность языка — это неточность модели: если команда говорит «аккаунт», эксперт — «клиент», а в базе usr, то уже на уровне разговора система несёт три несовместимых модели одного понятия.
Ограниченный контекст — явная граница, внутри которой модель и её язык имеют однозначный смысл. Один и тот же термин «клиент» в контексте продаж (человек с историей сделок и сегментом) и в контексте биллинга (юридическое лицо с платёжными реквизитами) — это две разные модели, и DDD запрещает сливать их в одну «общую»: попытка построить единую модель на всю систему рождает перегруженные сущности, в которых каждое поле «иногда значит». Контекст — единица владения: у него есть команда-хозяин, свой код и — в зрелой системе — своё хранилище.
Карта контекстов — диаграмма того, как контексты связаны: кто кому поставляет модель, кто под кого подстраивается, где между ними защитный слой. Карта — главный артефакт стратегического DDD: она делает неявные зависимости явными и служит исходным материалом для архитектурных решений (и их фиксации в ADR).
Поддомен — часть предметной области бизнеса, а не системы. Эванс делит их на три типа, и это деление — инвестиционная карта проекта:
| Тип поддомена | Что это | Инвестиция | Типичное решение |
|---|---|---|---|
| Core (ядро) | То, ради чего бизнес выигрывает у конкурентов | Максимальная: лучшие разработчики, глубокое моделирование | Строим сами, лучшей командой |
| Supporting (поддерживающий) | Необходим для работы ядра, преимущества не даёт | Умеренная: достаточно хорошего качества | Строим сами, без избыточной зрелости |
| Generic (общий) | Стандартные для всей отрасли вещи: аутентификация, email, биллинг | Минимальная | Покупаем: SaaS, open-source, вендор |
На примере интернет-магазина: core — ценообразование и промо-движения (то, за счёт чего магазин обходит конкурентов); supporting — каталог и обработка заказов (нужны, но преимущества не дают); generic — аутентификация, email, платёжный шлюз (одинаковы у всех в отрасли). Команда, вкладывающая равные усилия во все три типа, гарантированно перерасходует на generic и недорабатывает core.
Ключевое различие, которое чаще всего теряют: поддомен — это проблема, ограниченный контекст — решение. Поддомены описывают, что делает бизнес (аналитическая категория: они существуют независимо от ПО), контексты — как система это моделирует (проектная категория: их проводит команда). В идеале границы контекстов совпадают с границами поддоменов, но в реальности бывает иначе: один поддомен размазан по трём контекстам легаси, а один контекст покрывает два поддомена.
| Поддомен | Bounded Context | |
|---|---|---|
| Природа | Проблема: часть предметной области | Решение: граница модели, языка и кода |
| Существует без ПО | Да — это часть бизнеса | Нет — артефакт проектирования |
| Кто определяет | Бизнес-реальность | Команда в ходе моделирования |
| Отвечает на вопрос | «Что делает бизнес?» | «Как мы это моделируем?» |
| Меняется при рефакторинге | Нет | Да |
Расхождение между стороной проблемы и стороной решения — само по себе диагноз: карта «поддомен → контекст» показывает, где моделирование ведёт чужой домен не той моделью.
Схема связи понятий стратегического уровня:
ПРЕДМЕТНАЯ ОБЛАСТЬ (domain)
│
├── Поддомены — сторона проблемы: «что делает бизнес»
│ ├── Core — ценообразование и промо-движения (ядро)
│ ├── Supporting — каталог, обработка заказов (поддержка)
│ └── Generic — идентификация, email, платежи (стандарт отрасли)
│
└── Ограниченные контексты — сторона решения: «как мы это моделируем»
├── «Продажи» — модель + язык + код (владеет понятием «заказ»)
├── «Биллинг» — модель + язык + код (владеет понятием «счёт»)
└── «Идентичность» — купленный SaaS (чужая модель)
Реализация: Core «промо-движения» ──реализуется──▶ контекст «Продажи»
Generic «идентификация» ──закрывается───▶ контекст «Идентичность»
Карта контекстов:
«Продажи» ──── customer/supplier ────▶ «Биллинг»
«Продажи» ──── антикоррупционный слой ──▶ «Идентичность» (защита от чужой модели)
«Продажи» ──── опубликованный язык ───▶ «Доставка»
Карта контекстов фиксирует характер связи между частями системы; сами типы связей — мини-словарь Эванса:
- Partnership — равноправное партнёрство: контексты меняются согласованно, команды координируются.
- Customer–Supplier — один контекст заказывает (customer), другой поставляет (supplier); у заказчика есть голос в планах поставщика.
- Conformist — подстройка под чужую модель без влияния на неё: типично при интеграции с большим внешним сервисом.
- Anti-corruption layer (антикоррупционный слой) — переводчик между чужой моделью и своей; главный инструмент интеграции с легаси и внешними SaaS: чужие понятия не просачиваются в домен.
- Shared kernel — небольшой общий фрагмент модели, поддерживаемый совместно; используется экономно, потому что создаёт взаимную зависимость.
- Open Host Service + Published Language — открытый протокол интеграции с опубликованной схемой (например, REST API с версионируемым контрактом).
С чего начинают стратегическое моделирование на практике? С событий и разговоров. Event Storming Брандолини — самая распространённая точка входа: эксперты и разработчики за один-два дня выкладывают на доску поток доменных событий, и «горячие точки» — места, где возникают вопросы и споры, — сами очерчивают кандидатные границы контекстов. Более традиционный путь — серия структурированных интервью с экспертами и чтение рабочих документов (регламентов, инструкций, ТЗ), из которых извлекаются термины-кандидаты языка. В обоих случаях первый результат — не готовая архитектура, а черновик карты: поддомены-кандидаты, контексты-кандидаты, список спорных понятий. Черновик уточняется итерациями — и это нормально: карта контекстов живёт столько же, сколько система.
Выбор типа связи — архитектурное решение: conformist дешевле антикоррупционного слоя, но принимаете чужую модель как данность; ACL дороже, но защищает ядро. Такое решение стоит фиксировать в ADR с обоснованием.
Тактическое проектирование
Тактический уровень даёт строительные блоки кода доменной модели. Каждый блок решает конкретную задачу моделирования; ниже — определения и ориентиры «когда применять».
Entity (сущность). Объект с уникальной идентичностью, которая сохраняется во времени и между состояниями. Два заказа с одинаковым составом — всё равно два разных заказа; идентичность живёт в ID, а не в значениях полей. Сущность меняется, оставаясь «собой»: заказ №42 проходит статусы «черновик → размещён → отгружен», и это один и тот же объект. Когда применять: понятие домена уникально, отслеживается и меняет состояние во времени — заказ, клиент, договор, платёж.
Value Object (объект-значение). Объект без идентичности, целиком описываемый своими значениями; как правило, неизменяемый (immutable): «изменить» его — значит создать новый экземпляр. Деньги, адрес, период дат, тарифная зона — всё это значения. У значений могут быть свои правила: Money разных валют нельзя складывать, период не может быть «отрицательной длины» — это инварианты значения, и живут они внутри него. Когда применять: важны только величины и правила над ними, а не «какая именно это штука». Value object — самый недооценённый блок DDD: инстинкт «на всё заводить сущность с ID» раздувает модель и лишает её точности, тогда как значения упрощают рассуждения и убирают целый класс мутационных багов.
Aggregate (агрегат) и Aggregate Root (корень агрегата). Кластер сущностей и значений, изменяемый как единое целое; корень — единственная точка входа: внешний код ссылается только на него, инварианты кластера проверяются только в нём. Агрегат вводится ради одного — границы согласованности (подробнее — следующий раздел). Когда применять: есть инвариант, который обязан выполняться всегда («сумма заказа равна сумме позиций», «активная подписка одна на клиента») и который нельзя доверять разрозненным объектам.
Repository (репозиторий). Абстракция хранения агрегатов, предоставляющая иллюзию коллекции в памяти: orders.byId(id), orders.add(order). Скрывает базу данных за интерфейсом, принадлежащим домену: сигнатуры методов говорят на языке модели, а не на языке запросов. Когда применять: работа с целыми агрегатами по идентичности; для запросов на чтение без бизнес-правил репозиторий не обязателен — там уместны query-сервисы (см. раздел о CQRS).
Domain Service (доменный сервис). Операция домена, не принадлежащая ни одной сущности: она координирует несколько агрегатов или реализует вычисление, которому «неудобно жить» в одном объекте (перерасчёт тарифов по всей корзине клиентов). Доменный сервис не имеет состояния и говорит на языке домена — в отличие от прикладного сервиса (application service), который лишь оркестрирует транзакцию: загрузить агрегат, вызвать метод, сохранить, издать события. Когда применять: осторожно и как можно реже — каждый перенос логики из сущности в сервис приближает анемичную модель.
Domain Event (доменное событие). Неизменяемый факт, значимый для домена и случившийся в прошлом: OrderPlaced, PaymentCaptured, SubscriptionSuspended. Именуется в прошедшем времени; издаётся агрегатом при изменении состояния. Событие — клей между агрегатами и контекстами: на него подписываются обработчики, которые выполняют свои транзакции. Когда применять: другие части системы должны узнавать об изменении; развязка контекстов; построение итоговой согласованности; аудит.
Factory (фабрика). Инкапсуляция сложного создания агрегата или сущности, когда конструктор не справляется: восстановление инвариантов, сборка из нескольких источников, валидация составных частей. Когда применять: создание нетривиально; в простых случаях достаточно конструктора — фабрика «на каждый случай» лишь добавляет церемоний.
Сводная таблица блоков:
| Блок | Роль в модели | Ключевое свойство |
|---|---|---|
| Entity | Идентифицируемый участник домена | Идентичность; жизнь во времени; изменяемость |
| Value Object | Неизменяемое значение с правилами | Равенство по значениям; immutable |
| Aggregate / Root | Граница согласованности кластера | Единая точка входа; защита инвариантов |
| Repository | «Коллекция» агрегатов в памяти | Скрывает хранение за интерфейсом домена |
| Domain Service | Доменная операция без своего места | Без состояния; язык домена |
| Domain Event | Свершившийся факт домена | Прошедшее время; связывает агрегаты и контексты |
| Factory | Сложное создание объектов | Инкапсуляция конструирования |
Сравнение двух главных кандидатов моделирования — сущности и объекта-значения:
| Критерий | Entity | Value Object |
|---|---|---|
| Идентичность | Есть (ID), живёт во времени и между состояниями | Нет — объект целиком определяется значениями |
| Равенство | По идентификатору | По значениям всех полей |
| Изменяемость | Меняет состояние, оставаясь «собой» | Immutable: изменение = новый объект |
| Жизненный цикл | Создаётся, живёт, архивируется | Создаётся на лету и отбрасывается |
| Хранение | Своя строка/документ, репозиторий | Часто внедряется в владельца |
| Пример | Заказ №42, клиент, договор | Money(1000, "RUB"), адрес, период дат |
| Моделировать как, если… | Уникальность и история важны | Важны только значения и правила над ними |
Практический ориентир Вернона: начинать моделирование со значений и переходить к сущностям только там, где без идентичности нельзя обойтись. Обратный порядок (всё — сущности) даёт модель, перегруженную идентичностями и таблицами, где естественны были бы три поля-значения.
И последнее правило тактического уровня, стоящее особняком: модель — не данные. Команда, начинающая проектирование с ER-диаграммы («сначала таблицы, потом классы»), получает data-driven дизайн: логика расползается по сервисам-процедурам, а классы превращаются в контейнеры полей — анемичную модель (anemic domain model, термин Мартина Фаулера). DDD переворачивает порядок: сначала модель поведения и инвариантов, потом — как её сохранить. Таблица базы — одна из проекций модели, а не её источник.
В коде контекст обычно оформляется как отдельный модуль с внутренней структурой, отражающей роли: domain (сущности, значения, агрегаты, события, интерфейсы репозиториев), application (прикладные сервисы — сценарии), infrastructure (реализации репозиториев, шины, адаптеры) и interfaces или api (контроллеры, сообщения). Это не предписание Эванса, а устоявшаяся конвенция сообщества; важно в ней одно: зависимости направлены внутрь, к domain, — тот же принцип, что в Clean Architecture, о связи с которой — отдельный раздел ниже.
Агрегаты и границы согласованности
Агрегат — сердце тактического DDD и главный источник ошибок при его применении. Напомним определение: агрегат — кластер объектов с границей согласованности: внутри границы инварианты выполняются всегда и немедленно, за границей — только когда-нибудь, через события. Агрегат «Заказ» гарантирует, что сумма заказа равна сумме позиций, в любой момент между транзакциями; согласованность «заказ оформлен → счёт выставлен» между агрегатами «Заказ» и «Счёт» гарантировать транзакционно нельзя — и DDD честно запрещает пытаться.
Схема агрегата на примере заказа:
┌────────────── Агрегат «Заказ» ──────────────┐
│ │
снаружи ──▶│ Order (aggregate root) │
│ ├── lines: OrderLine × N (entity) │
│ ├── total: Money (value) │
│ └── delivery: Address (value) │
│ │
│ Инвариант: total = Σ(line.price × qty) │
│ Инвариант: размещённый заказ нельзя │
│ изменять без смены статуса │
└────────────────────────────────────────────┘
│
│ ссылка на Customer и Warehouse —
│ только по идентификатору (Id)
▼
другие агрегаты — свои транзакции,
свои гарантии, связь через события
Два уровня согласованности удобно держать перед глазами в виде таблицы:
| Внутри агрегата | Между агрегатами | |
|---|---|---|
| Гарантия | Немедленная, транзакционная | Итоговая (eventually consistent) |
| Кто обеспечивает | Корень агрегата при каждом изменении | Доменные события и подписчики |
| Пример | total = Σ позиций заказа | «счёт выставлен после размещения заказа» |
| Цена | Блокировки, конкуренция за агрегат | Задержка видимости; UX-компенсации |
Правила обращения с агрегатами восходят к Эвансу («одна транзакция модифицирует один агрегат») и доведены до практической формы Верноном в статьях Effective Aggregate Design (2010) и книге Implementing Domain-Driven Design (2013). Выдержка правил:
- Защищайте внутри агрегата только истинные инварианты. Истинный инвариант — бизнес-правило, которое обязано выполняться всегда, немедленно и при любой конкуренции. Если правило на деле допускает минуту рассинхрона (например, «у товара отображается примерно актуальный остаток») — это не инвариант, и объекты не обязаны жить в одном агрегате.
- Проектируйте маленькие агрегаты. Агрегат должен быть настолько мал, насколько это возможно, чтобы защитить свои истинные инварианты. Большой агрегат — это блокировки на каждую транзакцию, конфликты конкурирующих пользователей и падение производительности; инстинкт «собрать всё в один объект ради согласованности» — самая частая ошибка тактического DDD.
- Ссылайтесь на другие агрегаты только по идентичности. Заказ хранит
customerId, а не объектCustomer. Ссылка по объекту тащит за собой чужой граф, чужие блокировки и соблазн изменить чужой агрегат в своей транзакции — все три нарушения границ сразу. - Одна транзакция — один агрегат. Команда (пользовательское намерение) модифицирует ровно один экземпляр агрегата и фиксируется. Всё, что должно произойти «вслед за этим», — не в этой транзакции.
- Между агрегатами — итоговая согласованность через доменные события. Агрегат издаёт
OrderPlaced; подписчики (в том числе в других контекстах) реагируют своими транзакциями. Если бизнесу нужна мгновенная видимость, значит бизнес сам выбрал уровень гарантии — команда проектирует UX вокруг этого (кнопка «заказ принят», счёт появится через секунду), а не растягивает транзакцию на два агрегата.
Классическая иллюстрация завышенной границы — агрегат Customer, содержащий все заказы клиента «чтобы всегда видеть их сумму». Такая модель ломается первым же наплывом пользователей: конкурентные заказы одного клиента сериализуются на блокировке Customer, каждая операция с заказом перезагружает историю всех заказов, а агрегат раздувается до нечитаемости. Правильная граница: Customer и Order — отдельные агрегаты; «сумма заказов клиента», если она нужна немедленно точной, — это вопрос к бизнесу о цене этой точности, а чаще всего — read-модель, обновляемая событиями.
Практический пример различия истинных и ложных инвариантов: правило «сумма заказа равна сумме позиций» — истинное: иначе оформление заказа бессмысленно; оно принадлежит агрегату «Заказ». Правило «на складе достаточно товара» в интернет-магазине почти никогда не является истинным инвариантом заказа: остатки меняются каждую секунду чужими заказами, и система честнее работает по схеме:
- Агрегат «Заказ» проверяет свои инварианты и издаёт
OrderPlaced. - Контекст «Склад» получает событие и пытается зарезервировать товар.
- При успехе издаётся
GoodsReserved; при неуспехе —ReservationRejected. - «Заказ» реагирует сменой статуса (
reserved/cancelled) и уведомлением клиента.
Ни один шаг не растягивает транзакцию на чужой агрегат; согласованность достигается за секунды, а конфликт заказов за последний товар решается бизнес-правилом (кто первый, компенсация, лист ожидания), а не блокировкой на две таблицы.
Бонус малого агрегата — тестируемость. Агрегат без внешних ссылок ведёт себя как чистая функция от команды: загрузили корень из заглушки-репозитория, выполнили команду, проверили состояние и изданные события — без базы, шины и сети. Такие тесты пишутся в терминах языка домена (order.place() порождает OrderPlaced и запрещает пустой заказ), выполняются за микросекунды и служат регрессионной сеткой именно для бизнес-правил — самого ценного кода системы.
Связь с микросервисами
Стратегический DDD и микросервисная архитектура связаны теснее, чем любые два других подхода этой базы знаний: вторая волна популярности DDD в 2010-х случилась именно потому, что bounded context оказался готовым ответом на самый трудный вопрос микросервисов — где проводить границы сервисов. Технические критерии (нагрузка, стек, размеры команд) дают границы, которые постоянно приходится перекраивать; граница по домену — «этот сервис владеет понятиями продаж» — стабильна, потому что стабильна структура бизнеса.
Рабочее правило звучит так: ограниченный контекст — ориентир границы сервиса, но не тождество:
| Bounded Context | Микросервис | |
|---|---|---|
| Что ограничивает | Модель и язык | Развёртывание и владение |
| Природа границы | Логическая | Физическая |
| Меняется при | Уточнении модели | Требованиях эксплуатации |
| Соотношение | Первичен | Производен: сервисов может быть больше, меньше или столько же |
Один контекст может быть реализован несколькими сервисами (когда, например, read-модель вынесена отдельно); несколько тесно связанных контекстов могут жить в одном сервисе на этапе, когда система мала. Контекст — логическая граница модели и языка; сервис — физическая граница развёртывания; первое первично, второе производно и принимается с учётом дополнительных критериев: командных границ, нагрузки, требований доступности.
Симметричное правило касается данных: контекст владеет своей моделью — значит, сервис, реализующий контекст, владеет своей базой данных. Общая таблица, читаемая и пишущаяся несколькими сервисами, разрушает границу контекста: модель перестаёт принадлежать одному владельцу, язык перестаёт быть единым, любое изменение схемы становится межкомандной операцией. Интеграция между контекстами идёт через контракты (API, события) — как раз те типы связей из карты контекстов, что описаны выше.
Границы контекстов определяют и организационную структуру: команда владеет контекстом целиком — моделью, кодом, языком, базой. Это прямое применение закона Конвея в свою пользу (reverse Conway manoeuvre): организация проектируется под желаемую архитектуру, а не получает архитектуру как отпечаток случайной структуры отделов. Команда на полконтекста — это вечные согласования; команда на три контекста — это очередь из изменений к одному владельцу.
Технически интеграция контекстов в микросервисном мире чаще всего событийная: брокер (Kafka, RabbitMQ) разносит OrderPlaced подписчикам. Здесь работает то же правило границ, что и в коде: подписчик на чужие события обязан ставить между ними и своей моделью собственный антикоррупционный слой — переводить OrderPlaced чужого словаря в свои понятия на входе. Прямая подписка «в лоб» делает контекст conformist’ом по отношению к каждому продюсеру: смена чужой схемы событий мгновенно становится менять вашу модель — граница, которая так дорого обходилась при проектировании, разрушается в одном merge request.
Отдельное предостережение: слепое правило «один контекст = один сервис», применённое без анализа связей, порождает распределённый монолит — сервисы, которые нельзя деплоить независимо, потому что каждая операция проходит через три из них синхронно. Признак здоровой декомпозиции по доменам: типовая операция изменяет один сервис-контекст, а остальные узнают о ней из событий. Если для оформления заказа последовательно вызываются «корзина», «заказы», «склад» и «биллинг» — границы проведены не по доменам, а по экранам интерфейса, и стоимость распределённости не окупается. В сомнительных случаях разумной промежуточной формой остаётся модульный монолит: границы контекстов проводятся в коде и защищаются модульно, а превращение их в сервисы откладывается до реальной необходимости.
Проверить proposed-границу сервиса можно четырьмя вопросами:
- Владение данными: может ли команда изменить схему своих таблиц, не согласовывая ни с кем? Если нет — граница фиктивна.
- Язык: понимает ли команда-владелец все понятия своего сервиса без переводчика из другой команды? Если нет — это несколько контекстов, склеенных в один сервис.
- Развёртывание: можно ли выложить релиз сервиса, не выкладывая соседей? Синхронный релизный поезд — признак распределённого монолита.
- Отказ: переживает ли бизнес-процесс недоступность соседнего сервиса (пусть деградируя)? Если любой сбой соседа останавливает всё — связность выше заявленных границ.
Связь с Clean Architecture и CQRS
DDD отвечает на вопрос «какой должна быть модель», но не на вопрос «как изолировать её от инфраструктуры» — это задача Clean Architecture и её родственников (гексагональной архитектуры, Onion). На практике подходы складываются в типичный союз: тактические паттерны DDD живут во внутреннем ярусе Clean Architecture, а правило зависимостей защищает их от фреймворков и баз данных. DDD даёт содержание ядра, Clean Architecture — его защиту.
Соответствие ролей почти взаимное:
| DDD | Clean Architecture | Комментарий |
|---|---|---|
| Доменная модель: сущности, значения, агрегаты | Entities (кольцо правил предприятия) | Самое стабильное и ценное ядро |
| Прикладные сервисы (application services) | Use Cases / Interactors | Оркестрация сценариев без бизнес-решений |
| Repository (интерфейс) | Boundary-интерфейс шлюза | Объявлен внутри, реализован снаружи |
| Реализация репозитория (ORM, SQL) | Gateway в слое адаптеров | Зависимость направлена внутрь |
| Domain Event | — (оркестрируется интерактором или шиной) | Точка расширения за пределами колец |
Плод этой связки — формула Мартина «база данных — это деталь» получает в DDD конкретный механизм: репозиторий. Домен объявляет OrderRepository с методами в своих терминах, реализация с ORM и SQL живёт снаружи, в слое адаптеров. Доменная модель не знает о таблицах; таблицы — одна из проекций модели, а не её источник.
CQRS (Command Query Responsibility Segregation) — разделение операций записи и чтения на разные модели — развивается из DDD естественно. Командная сторона — это классический DDD: команды (PlaceOrder) загружают агрегат через репозиторий, выполняют инварианты, фиксируют, издают доменные события. Запросная сторона чаще всего обходит домен: у чтения нет инвариантов, поэтому query-обработчик идёт из контроллера прямо к оптимизированному представлению данных — денормализованной read-модели, проекции, поисковому индексу. Доменные события — транспорт, которым командная сторона обновляет read-модели: OrderPlaced пересчитывает проекцию «активные заказы клиента».
Пример: экран «история заказов клиента» не грузит тысячи агрегатов «Заказ» через репозиторий, а читает одну строку из проекции customer_orders_summary, которую поддерживает обработчик событий. Запись остаётся консистентной и защищённой инвариантами; чтение — быстрым и специализированным под конкретный экран.
С CQRS смыкается Event Sourcing — хранение агрегата не в виде «текущего состояния», а в виде последовательности доменных событий, его породивших: текущее состояние — функция свёртки событий. Для DDD это привлекательный, но не обязательный спутник: события становятся единственным источником истины, аудит и «проигрывание истории» бесплатны, но цена — совершенно другая модель хранения, консистентность проекций и сопровождаемость. Обоснованное место CQRS и Event Sourcing — системы с высокой нагрузкой на чтение, асимметричные сценарии чтения/записи, требования аудита; применённые «для моды» в CRUD-приложении, они умножают сложность без выгоды. Важно понимать: CQRS и Event Sourcing не входят в DDD и не требуются для него — большинство DDD-систем живут без них, с обычными репозиториями и реляционной базой.
Сигнал, что проектному чтению пора усложняться, — боль, а не мода: списки и фильтры начали требовать индексы и JOIN’ы, которые не влезают в репозиторий; один и тот же агрегат читают пять экранов с разными требованиями к форме данных; нагрузка на чтение на порядок выше записи. До этих сигналов честнее оставаться на «одна модель на запись и чтение» — CQRS, применённый заранее, лишь удваивает количество моделей, которые придётся поддерживать.
Ubiquitous Language на практике
Единый язык — самый ценный и самый хрупкий артефакт DDD: его нельзя «внедрить» один раз, его можно только поддерживать. Практика сводится к трём дисциплинам.
Именование в коде. Термины языка становятся именами классов, методов, полей и тестов. Проверка проста: фрагмент доменного кода зачитывается эксперту вслух — тот должен понять, о чём речь, без расшифровки. Если разработчик вынужден «переводить» имя класса на человеческий язык — имя неверное либо модель неточная. То же работает в обратную сторону: когда эксперт в разговоре употребляет новый термин, которого нет в модели, — это не «синоним», это сигнал, что в домене есть понятие, которое модель ещё не различает.
| Эксперт говорит | Код «с переводом» | Код на едином языке |
|---|---|---|
| «приостановка обслуживания» | updateStatus(2) |
suspendService() |
| «списание подтверждено» | flag = true |
payment.capture() |
| «льготный период» | graceFlag |
GracePeriod (value object) |
| «блокировка клиента» | state = 9 |
ClientSuspended (domain event) |
Код-ревью по языку. Расхождение языка и кода — дефект, и ловится он дешевле всего в ревью. Практический чек-лист ревьюера DDD-проекта: как эксперт назовёт этот метод? Есть ли в изменении термин, отсутствующий в глоссарии? Не появился ли «перевод» — термин, которым команда обозначает понятие внутри себя, отличное от того, каким его называют эксперты? Такие замечания поначалу кажутся придирками к стилю — пока в третий раз не выяснится, что «блокировка» в понимании бэкенда и «блокировка» в понимании риск-отдела — два разных состояния с разными последствиями. Имена тестов — часть той же дисциплины: cannot_place_empty_order говорит на языке домена и фиксирует язык лучше любого документа.
Глоссарий как живой документ. Словарь языка ведётся рядом с кодом — в вики, README контекста или наборе ADR — и принадлежит конкретному ограниченному контексту: «клиент» в глоссарии продаж и «клиент» в глоссарии биллинга — отдельные статьи, каждая со своим определением. Глоссарий устаревает ровно тогда, когда его перестают сверять с кодом; поэтому полезна связка «изменился термин — правится и глоссарий, и код» в одном PR.
Язык живёт не только в коде и глоссарии, но и во всей коммуникации вокруг системы: в названиях задач и багов («заказ не проходит capture», а не «ошибка статуса №7»), в коммит-сообщениях, в докладах команды и письмах экспертам. Место, где язык перестаёт употребляться, — место, где он начинает расходиться с моделью: если в трекере один словарь, в коде второй, а на созвоне с экспертами третий — у команды фактически три модели, и ни одна не является системой.
Типичная ошибка — «перевод» между командой и экспертами. Команда вырабатывает свой технический жаргон (user_state = 2, «прогрев кэша клиентов»), эксперты говорят на своём («клиент на паузе»), а между двумя словарями живёт неявный переводчик — обычно один-два человека, которые «понимают обоих». Каждый акт перевода теряет смысл, а носитель перевода — это bus-factor и узкое место любых переговоров. DDD требует устранить перевод как класс: либо термин эксперта становится термином кода, либо код убедил эксперта переименовать понятие — но в любой момент времени у понятия ровно одно имя.
Микро-пример того, как язык уточняет модель: эксперт дважды называет «оплаченным» и заказ с захолдированными средствами, и заказ с поступившими средствами. Правильная реакция — не уточняющий вопрос «а что вы имели в виду?», а разделение терминов: authorized и captured; уточнённый язык поднимает точность модели и убирает класс будущих дефектов. Каждое такое уточнение — маленькая победа моделирования; накопленные сотнями, они и составляют разницу между системой, которая «примерно отражает бизнес», и системой, которая является его моделью.
Когда DDD избыточен
Честный список противопоказаний — половина пользы знакомства с DDD, потому что главная цена подхода платится там, где он не нужен.
CRUD-домены. Если приложение — формы над базой данных с минимумом правил (админки, реестры, каталоги без сложной логики), вся тактическая машина DDD — агрегаты, репозитории, события — добавляет структуру без содержания: инвариантов нет, защищать нечего, и «агрегат» вырождается в DTO с лишним слоем. Здесь честный controller → service → repository решает задачу дешевле.
Простые и короткоживущие системы. Прототипы, MVP, лендинги, одноразовые интеграции. У них нет горизонта жизни, на котором инвестиция в модель окупается: модель уточняется итерациями, а итераций у системы три. Стартап на этапе поиска product-market fit меняет домен быстрее, чем его можно смоделировать: сама бизнес-модель пересматривается каждые пару месяцев, и «точная модель» фиксирует понимание, которое завтра устареет. Сначала — гипотеза и скорость; DDD приходит, когда домен устоялся, а система — осталась.
Нет доступа к экспертам. DDD не работает без постоянного диалога с носителями домена: без него модель деградирует в фантазию архитектора. Это не «желательно», а предусловие — недоступность экспертов означает не «DDD похуже», а «не DDD».
Цена DDD. Даже там, где он оправдан, подход платится реально: обучение команды словарю и паттернам; сессии совместного моделирования, съедающие время экспертов; маппинг между моделями контекстов на каждой интеграции; дисциплина языка, которую нужно поддерживать годами. Окупаемость измеряется месяцами и годами — снижением стоимости изменений долгоживущей системы, а не скоростью первых спринтов.
Сводный критерий применения — три условия сразу:
| Условие | Вопрос-проверка | Если нет |
|---|---|---|
| Сложный домен | Много ли правил, инвариантов, состояний и переходов? | DDD добавит структуру без содержания |
| Долгоживущая система | Будет ли система развиваться годами? | Инвестиция в модель не успеет окупиться |
| Доступ к экспертам | Готов ли бизнес участвовать в моделировании? | Модель станет фантазией архитектора |
Выполнены все три — DDD окупается; не выполнено хотя бы одно — выгоднее более лёгкий подход. Полезна и частичная стратегия: применить только стратегический уровень (границы контекстов и язык) без тактических паттернов — это дёшево и приносит значительную часть пользы; обратный порядок (тактика без стратегии) не приносит почти ничего.
Если система выросла и DDD перестал окупаться — «вырулить обратно» тоже нормальный ход: тактические паттерны локальны, их можно постепенно упрощать в отдельных модулях (агрегат без инвариантов спокойно становится обычной таблицей с CRUD), сохраняя стратегические границы и язык. DDD — инструмент с настройкой глубины, а не тумблер «вкл/выкл»: уровень применения выбирается по сложности конкретной части системы, и разные контексты одной системы вполне могут жить на разных уровнях — core с полной тактической дисциплиной, supporting — с облегчённой, generic — вообще без неё, в купленном SaaS.
Плюсы
- Код говорит на языке бизнеса. Единый язык устраняет «перевод» между экспертами и командой; требования, обсуждения и код используют один словарь, что сокращает недопонимание — скрытую причину доброй половины дефектов в сложных доменах.
- Управляемая сложность. Bounded contexts делят большую систему на части, каждая из которых моделируется осмысленно в своих границах; команда работает с управляемым фрагментом модели, а не со всей системой сразу.
- Явные границы согласованности. Агрегаты делают инварианты явными, проверяемыми и локализованными: правило живёт в объекте, который за него отвечает, а не размазано по сервисам-процедурам.
- Тестируемость домена. Чистый доменный слой с репозиториями-интерфейсами тестируется без базы и сети: поведение модели проверяется как чистая логика, быстро и детерминированно.
- Осмысленные границы микросервисов. Bounded context даёт стабильную, основанную на бизнесе границу сервисов и баз данных — вместо границ «по экранам» и «по слоям», которые приходится перекраивать.
- Долгоживущая модель и дешёвый онбординг. Код, названный терминами экспертов, читается как описание бизнеса; новый разработчик изучает домен, читая доменный код, а глоссарий контекста служит живой документацией.
- Инвестиционная карта. Деление на core / supporting / generic даёт явный ответ, куда направлять лучшие силы, а что покупать готовым, — предотвращая недоинвестирование ядра и перерасход на стандартных вещах.
- Эволюционность. Итеративное уточнение модели внутри стабильных границ позволяет системе меняться вместе с бизнесом без цепных реакций по всему коду.
- Готовая основа для аналитики и аудита. Доменные события — журнал значимых бизнес-фактов «из коробки»: на них строятся аудит, метрики воронки и аналитика без отдельной подсистемы логирования смысла.
Минусы
- Высокий порог входа. DDD требует навыка моделирования: слушать эксперта, находить точные имена, чувствовать границы агрегатов, различать сущность и значение. Без этого навыка подход вырождается в карго-культ — агрегаты и репозитории без эффекта.
- Цена тактических паттернов. Маппинг между моделями контекстов, интерфейсы репозиториев, события, обработчики, проекции — заметный объём инфраструктурного кода, который в простом домене превышает саму логику.
- Жёсткая зависимость от экспертов. Модель строится в диалоге; недоступность носителей домена обнуляет подход — модель становится фантазией, красиво закодированной.
- Долгий ROI. Первые месяцы DDD-проект медленнее «просто писать код»: сессии моделирования, уточнение языка, проектирование контекстов. Оценка по скорости первых спринтов — типичная причина преждевременного отказа от подхода.
- Избыточность для CRUD. В доменах без инвариантов и сложных правил тактический DDD — чистые издержки: структура без содержания.
- Риск анемичной модели. При механическом применении логика уезжает в сервисы, а доменные объекты остаются мешками полей с геттерами — формально DDD, по сути процедурный код в объектной обёртке (антипаттерн anemic domain model, описанный Фаулером).
- Итоговая согласованность усложняет UX. Правила «один агрегат на транзакцию» и события означают, что часть системы обновляется «когда-нибудь»; интерфейсы и ожидания пользователей приходится проектировать вокруг этой задержки.
- Тяжесть внедрения в легаси. Вводить контексты в «большой шар грязи» возможно (постепенно, через антикоррупционные слои), но это многомесячная программа, а не рефакторинг на спринт.
- Плохая совместимость с «дешёвыми» командами и аутсорсом. Подход требует вовлечённости в домен и долгоживущего владения моделью; команда, работающая по спецификации без доступа к экспертам и без ответственности за эволюцию модели, выжмет из DDD только церемонии.
Связанные статьи
- Микросервисы — декомпозиция системы на сервисы; bounded context как главный ориентир границы сервиса и базы данных.
- Clean Architecture — изоляция доменной модели DDD от инфраструктуры правилом зависимостей; репозиторий DDD как шлюз внешнего яруса.
- SOLID — принципы проектирования классов и модулей, на которые опираются тактические паттерны DDD.
- RDD — Risk Driven Development — риск-ориентированный ответ на вопрос «сколько DDD достаточно»: глубину моделирования определяет реестр рисков, а не мода.
- AIDD — AI-Driven Development: модель и единый язык DDD как контекст, в котором AI-генерация доменного кода остаётся осмысленной.
- Введение в архитектуру — общие понятия архитектуры ПО и место DDD среди подходов к проектированию.
- Монолитная архитектура — deployment-стиль, в котором границы контекстов DDD реализуются как модули (модульный монолит) до перехода к сервисам.
- DDD — Domain Driven Design — управленческий разбор подхода из серии driven-методологий: влияние на команды, планирование, инвестиции.