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). Выдержка правил:

  1. Защищайте внутри агрегата только истинные инварианты. Истинный инвариант — бизнес-правило, которое обязано выполняться всегда, немедленно и при любой конкуренции. Если правило на деле допускает минуту рассинхрона (например, «у товара отображается примерно актуальный остаток») — это не инвариант, и объекты не обязаны жить в одном агрегате.
  2. Проектируйте маленькие агрегаты. Агрегат должен быть настолько мал, насколько это возможно, чтобы защитить свои истинные инварианты. Большой агрегат — это блокировки на каждую транзакцию, конфликты конкурирующих пользователей и падение производительности; инстинкт «собрать всё в один объект ради согласованности» — самая частая ошибка тактического DDD.
  3. Ссылайтесь на другие агрегаты только по идентичности. Заказ хранит customerId, а не объект Customer. Ссылка по объекту тащит за собой чужой граф, чужие блокировки и соблазн изменить чужой агрегат в своей транзакции — все три нарушения границ сразу.
  4. Одна транзакция — один агрегат. Команда (пользовательское намерение) модифицирует ровно один экземпляр агрегата и фиксируется. Всё, что должно произойти «вслед за этим», — не в этой транзакции.
  5. Между агрегатами — итоговая согласованность через доменные события. Агрегат издаёт OrderPlaced; подписчики (в том числе в других контекстах) реагируют своими транзакциями. Если бизнесу нужна мгновенная видимость, значит бизнес сам выбрал уровень гарантии — команда проектирует UX вокруг этого (кнопка «заказ принят», счёт появится через секунду), а не растягивает транзакцию на два агрегата.

Классическая иллюстрация завышенной границы — агрегат Customer, содержащий все заказы клиента «чтобы всегда видеть их сумму». Такая модель ломается первым же наплывом пользователей: конкурентные заказы одного клиента сериализуются на блокировке Customer, каждая операция с заказом перезагружает историю всех заказов, а агрегат раздувается до нечитаемости. Правильная граница: Customer и Order — отдельные агрегаты; «сумма заказов клиента», если она нужна немедленно точной, — это вопрос к бизнесу о цене этой точности, а чаще всего — read-модель, обновляемая событиями.

Практический пример различия истинных и ложных инвариантов: правило «сумма заказа равна сумме позиций» — истинное: иначе оформление заказа бессмысленно; оно принадлежит агрегату «Заказ». Правило «на складе достаточно товара» в интернет-магазине почти никогда не является истинным инвариантом заказа: остатки меняются каждую секунду чужими заказами, и система честнее работает по схеме:

  1. Агрегат «Заказ» проверяет свои инварианты и издаёт OrderPlaced.
  2. Контекст «Склад» получает событие и пытается зарезервировать товар.
  3. При успехе издаётся GoodsReserved; при неуспехе — ReservationRejected.
  4. «Заказ» реагирует сменой статуса (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-методологий: влияние на команды, планирование, инвестиции.