19 апреля 2026
DDD чаще всего встречают в виде тактических паттернов: агрегаты, объекты-значения, репозитории. Это вершина айсберга. Под ней - стратегический дизайн, дающий язык для разговора между бизнесом, архитектурой и командами. Без стратегической части DDD превращается в ORM с пафосом. Со стратегической - становится мостом от бизнес-стратегии к коду и структуре команд. В эпоху AI-агентов этот мост критичен: терминология и бизнес-операции теперь часть спецификации, и агент, работающий без DDD, угадывает модель вместо того чтобы следовать ей.
Что такое DDD и чего он не делает
Domain-Driven Design - подход к проектированию, центральная идея которого: модель кода должна отражать модель предметной области. Термины бизнеса становятся терминами кода. Правила бизнеса становятся инвариантами в коде. Границы бизнес-зон становятся границами модулей и команд.
DDD состоит из двух частей:
- Стратегический дизайн - деление предметной области на поддомены, установление ограниченных контекстов, формирование единого языка, карта контекстов. Работа на уровне «где компания зарабатывает и как устроены бизнес-зоны».
- Тактический дизайн - агрегаты, объекты-значения, сущности, доменные события, фабрики, репозитории. Работа на уровне «как реализовать модель в коде».
Распространённое заблуждение: DDD = тактика. Команда читает Evans, узнаёт про агрегаты и репозитории, начинает размечать классы. Код внешне становится «DDD-style», но бизнес-модель не меняется, язык не формируется, границ нет. Cargo cult.
Тактика работает только поверх стратегии. Без стратегии агрегаты - просто классы с методами, репозитории - обёртки над ORM, единый язык - глоссарий, который никто не читает.
Что DDD не делает: не заменяет бизнес-стратегию (если компания не знает, где зарабатывает, DDD не поможет - он отразит эту неясность в коде), не гарантирует микросервисы, не заменяет UX-дизайн, не даёт пользы на простых CRUD-задачах.
Поддомены и конкурентное преимущество
Предметная область компании не единое целое. Это набор зон. В каких-то зонах бизнес зарабатывает, в каких-то - поддерживает инфраструктуру работы. DDD разделяет на три типа.
Основной поддомен (core) - зона, где компания зарабатывает деньги и имеет конкурентное преимущество. Это сложно, это уникально, это меняется в ответ на рынок. Для SaaS в финтехе - алгоритм скоринга. Для маркетплейса - логика подбора и цен. Для платёжной системы - обработка транзакций с anti-fraud. Работа над core никогда не завершается: бизнес постоянно улучшает эту зону. Инвестируются лучшие инженеры, строится собственная реализация, формируется доменная экспертиза внутри.
Универсальный поддомен (generic) - индустриальный стандарт. Зона с зрелыми готовыми решениями. Аутентификация, биллинг через Stripe, email через SendGrid, CMS. Инвестиция своих инженеров в generic - проигрыш: даже если напишете «свою Stripe», в неё придётся заложить всё, что Stripe уже сделал, плюс поддерживать десятилетиями.
Вспомогательный поддомен (supporting) - нужен для работы бизнеса, но не даёт преимущества. Админ-панели, внутренние инструменты, системы отчётности, HR. Кандидаты на: готовые продукты, аутсорсинг или CRUD-приложения с минимальными усилиями внутри.
Практически - для каждой зоны ответить:
- Это core, generic или supporting?
- Если core: в чём преимущество, где собственная экспертиза выигрывает?
- Если generic: какое готовое решение, как подключаем?
- Если supporting: какой минимум нужен, как дешевле всего закрыть?
Core получает сильных инженеров. Generic закрывается интеграцией. Supporting закрывается минимальными средствами.
Единый язык (Ubiquitous Language)
Единый язык - словарь терминов предметной области, одинаково используемый бизнесом, продуктом, архитекторами, инженерами и документацией. Термин не переводится между ролями: «subscription» в разговоре с CEO и в коде - одно и то же понятие.
Без единого языка: бизнес говорит «клиент», продукт говорит «пользователь», бэкенд называет User, БД хранит в accounts, интеграция с платёжкой называет customer. Каждая команда в переводе теряет смысл. Через полгода никто не помнит, что «клиент» в разговоре CEO - не то же самое, что User, потому что компания добавила B2B-клиентов, а User - это конечный пользователь внутри организации-клиента.
Единый язык живёт в:
- Документации предметной области (не технической).
- Моделях кода: имена классов, методов, модулей.
- API-контрактах: пути, параметры, ответы.
- Схеме БД: названия таблиц и полей.
- UI: тексты интерфейса для пользователя.
- Разговорах: митинги, код-ревью, спеки.
Формируется не декларативно («давайте составим глоссарий»), а через событийное моделирование или совместные практики, где бизнес и инженеры описывают предметную область вслух, находят противоречия, договариваются.
Выигрыш не только в ясности. Изменения в бизнесе (новая политика, сегмент, продукт) легче транслируются в код: новое понятие получает имя, имя появляется в модели, API, документации - без перевода и искажений.
Ограниченные контексты (Bounded Contexts)
Одна модель на всю предметную область не работает. «Клиент» в биллинге - подписчик с тарифом и платежами. «Клиент» в маркетинге - лид с источником трафика и стадией воронки. «Клиент» в support - тикет-автор. Пытаться поместить это в один Customer - путь к тысячестроковому классу.
Ограниченный контекст (bounded context) - явная граница, внутри которой модель и язык однозначны. Разные контексты могут использовать одно слово для разных понятий, и это нормально.
Карта контекстов (context map) показывает какие контексты существуют, что термины значат в каждом, как контексты связаны, какие паттерны взаимодействия.
Стратегические паттерны взаимодействия
- Partnership - две команды работают над связанными контекстами как партнёры, координируют изменения. Требует плотной координации.
- Customer-Supplier - один контекст поставляет сервис другому. Поставщик учитывает потребности клиента в планировании.
- Conformist - потребляющий принимает модель поставщика как есть. Дёшево, но рискованно: изменения поставщика бьют напрямую.
- Anti-Corruption Layer (ACL) - потребляющий добавляет слой-переводчик между внешней моделью и своей. Защищает внутреннюю модель от чужого словаря.
- Shared Kernel - два контекста используют общую модель в небольшой зоне. Требует очень плотной координации, применяется осторожно.
- Separate Ways - контексты намеренно не взаимодействуют. Когда интеграция дороже дублирования.
- Open Host Service / Published Language - контекст предоставляет публичный API с документированным языком; потребители встраиваются через контракт.
Карта контекстов - стратегический документ. По нему видно, где команды работают плотно, где независимо, где legacy блокирует развитие, где нужен ACL.
DDD как мост от стратегии к коду и командам
DDD часто воспринимают как инструмент для «красивой архитектуры»: правильные слои, чистые интерфейсы, уважаемые паттерны. Это побочный эффект, не суть. Суть в другом: DDD - мост от бизнес-стратегии к коду и структуре команд. Всё остальное - язык, паттерны, архитектурные решения - производные этого моста.
Цепочка:
Бизнес-стратегия определяет, где компания зарабатывает и куда движется. Где конкурентное преимущество, какие сегменты важнее, какие направления растут.
DDD переводит стратегию в модель предметной области. Core-поддомены (где преимущество) получают глубокое моделирование, единый язык, границы. Generic и supporting - минимальная проработка. Карта контекстов показывает связи.
Team Topologies распределяет команды по bounded contexts. Каждая Stream-aligned команда владеет одним контекстом: язык, модель, код, эксплуатация. Подробнее в статье «Team Topologies».
Архитектура - последняя остановка. Границы контекстов становятся границами модулей и сервисов. Язык - именами в коде и API. Interaction patterns - интеграционными паттернами. Архитектура не выбирается отдельно от предметной области; она производная DDD.
Порядок важен. Если начать с архитектуры («нам нужны микросервисы»), получается технически красивая система, плохо отражающая бизнес. Если начать с команд («наймём N инженеров, распределим по фичам»), команды привязаны к случайным границам. Если начать с бизнес-стратегии, но не пройти DDD-шаг, переход к командам и архитектуре идёт на ощупь.
DDD - не замедление на моделирование. Это экономия на последующих этапах, когда команды, архитектура и процессы перестают переделываться каждые полгода под новое понимание бизнеса.
Перевод DDD в архитектуру
Стратегический дизайн остаётся теорией без перевода в код. Что обязательно сохраняется:
Границы bounded contexts → границы в коде. Каждый контекст - отдельный модуль или сервис. Код одного контекста не импортирует код другого напрямую. Коммуникация - через публичные API контекста.
Единый язык → имена в коде, API, БД. Термин Subscription в модели - это класс Subscription в коде, endpoint /subscriptions, таблица subscriptions. Нет перевода. Нет SubscriptionDTO vs SubscriptionModel с разным смыслом. Нет БД, где это customer_plans, а в коде - Subscription.
Anti-Corruption Layer в коде. Если контекст взаимодействует с внешней системой (другим bounded context, legacy-системой, внешним API), между ними - слой перевода. Внешние термины превращаются во внутренние на границе. Изменения во внешней модели локализуются в ACL, не просачиваются во внутреннюю.
Доменные события как способ слабой связи. Вместо прямых вызовов между контекстами - события. Контекст публикует («подписка отменена»), заинтересованные подписываются и реагируют. Терминология события - из единого языка публикующего контекста.
Частый сбой: модель в документации одна, в коде другая. Команда обсуждает «клиент с подпиской», код оперирует User и Account. Новый инженер читает код, строит у себя модель User + Account, начинает разговаривать в этих терминах. Через полгода все говорят двумя разными языками в зависимости от собеседника. Восстановление в этой точке дороже, чем поддержание с начала.
Защитить терминологию помогает дисциплина в code review. PR, в котором появляется имя, не соответствующее единому языку, - повод для обсуждения. Либо имя приводится к языку, либо язык расширяется сознательно.
DDD в эпоху AI: почему теперь критичнее
AI-агенты работают с формализованным контекстом. Спецификация задачи - их вход. У них нет интуиции «что имеется в виду», они не могут спросить на митинге, не читают между строк. Что в спеке - то и получат.
Для людей неточная терминология - повод задать вопрос. «Клиент» в задаче - подписчик или посетитель? Инженер спрашивает, уточняет, делает. Для агента нет механизма «задать вопрос по бизнес-контексту»; вопросы агента - о технике, не о предметной области. Агент делает допущения и идёт дальше.
Поэтому терминология и бизнес-операции теперь - часть спецификации. Без DDD спека выглядит как «сделай что-то с клиентом, чтобы получилось X». Агент выбирает собственную модель «клиента» - обычно ту, что чаще встречается в обучающих данных, или ту, что кажется подходящей по контексту файла. Получается работающий код, не соответствующий бизнес-модели.
С DDD спека даёт семантику. Bounded context фиксирует, в какой части домена работаем. Ubiquitous language определяет термины. Инварианты агрегата сообщают, какие правила нарушать нельзя. События описывают, что происходит в других контекстах при этом изменении. Агент следует семантике, не изобретает.
Конкретный пример встраивания - Spec Kit, фреймворк spec-driven development. Он формально запрещает писать про технический стек в фазе specify именно потому, что спека должна говорить на языке домена, не технологий. Структура Spec Kit (spec.md → plan.md → task-файлы) повторяет DDD-слои: spec описывает что на языке предметной области, plan переводит в архитектурное решение, задачи - в конкретные изменения кода. Ubiquitous language и bounded contexts становятся обязательным входом фреймворка, без них spec-фаза вырождается в описание «сделай фичу X» без семантики.
Тактические паттерны
Инструменты реализации модели в коде. Работают только поверх стратегии.
Сущность (Entity) - отражение объекта реального мира, имеющего идентичность, которая сохраняется через время. Идентичность позволяет отличить один экземпляр от другого: два заказа с одинаковой суммой и одинаковыми позициями - всё равно разные заказы, если у них разные номера. Две сущности с одинаковыми атрибутами - разные, если разные id.
Сущность живёт внутри одного bounded context и работает только для него. Один бизнес-объект может существовать в нескольких контекстах с общей идентичностью, но разным содержанием. Заказ с номером ORDER-12345:
- Для бухгалтерии - сумма, состояние оплаты, налоговые атрибуты, выписанные документы.
- Для сотрудника склада - какие позиции упакованы, какие в процессе, какие ждут поступления.
- Для службы доставки - адрес, статус отправки, трек-номер.
Общий номер связывает контексты, но внутри каждого Order - разная сущность со своими атрибутами и поведением. Попытка вместить все атрибуты в один класс - путь к тысячестроковому Order, непонятному никому. Разделение на bounded contexts решает это явно: общий id связывает, модели различаются.
Агрегат - граница согласованности и транзакционной целостности, сформированная из одной или нескольких сущностей вместе с их объектами-значениями. В простых случаях агрегат и сущность совпадают один к одному: сущность User - это и есть агрегат User. В сложных - агрегат составной: корневая сущность плюс набор дочерних, которые меняются только вместе с корневой. Пример составного агрегата: Order с коллекцией OrderLine - позиции нельзя менять независимо от заказа, иначе ломаются инварианты (суммы, допустимые комбинации, состояние оплаты).
Ключевое свойство: сущности внутри агрегата изменяются только через корневую сущность и только в составе транзакции над агрегатом целиком. Это делает границу агрегата явной зоной согласованности: внутри инварианты обеспечиваются кодом, снаружи - другой агрегат с собственными правилами.
Транзакция - не больше одного агрегата за раз в большинстве случаев. Агрегат также визуализирует зону конкурентного конфликта: если два процесса меняют один агрегат, они конкурируют за него. Отсюда практический инструмент - оптимистичный или пессимистичный лок на агрегат (выбор зависит от частоты конфликтов и стоимости откатов) гарантирует согласованное состояние. Без явно выделенного агрегата неясно, где ставить лок и что защищать; с агрегатом lock-границы совпадают с границами инварианта.
Доменный сервис - для бизнес-операции, которая не принадлежит одному агрегату. Имя сервиса = имя операции в языке бизнеса: TransferMoney, PlaceOrder, CancelSubscription. Задача - точно определить, какую часть реального мира эта операция описывает, и отразить её в коде как цельную единицу.
Это одно из критических мест, где DRY-подход действительно важен. Если операция одинаковая с точки зрения бизнеса, но выполняется по-разному в зависимости от условий (тип клиента, регион, канал), различия идут внутрь сервиса как условия - не распадаются на несколько параллельных сервисов с одинаковым бизнес-смыслом. Единая бизнес-операция - единый domain service; ветвления по условиям - внутри.
С обратной стороны: разные бизнес-операции не должны схлопываться в одну, не отражающую бизнес-смысл. Антипаттерн - сервис UpdateOrder, одновременно отвечающий за обновление позиций в заказе, приём платежа, обработку доставки на склад. Каждая из этих операций - отдельная бизнес-сущность со своими инвариантами, событиями и потенциально разными командами-владельцами. Слияние в один сервис теряет семантику и ломает выравнивание с языком бизнеса.
Пример: перевод денег между двумя аккаунтами - TransferMoney domain service. Он оркестрирует два агрегата (списание с одного, начисление на другой), обеспечивает согласованность, публикует событие. Это не метод одного агрегата и не utility-класс - это отражение бизнес-операции.
Объект-значение (Value Object) - объект, описывающий что-то, но не имеющий собственной идентичности. Противоположность Entity: Entity отвечает на вопрос «кто это» или «что это конкретно», VO - на вопрос «какое оно».
Два VO с одинаковыми атрибутами - одинаковы, это одно и то же значение. $100 USD - это $100 USD, независимо от того, сколько раз создаётся в коде. Адрес «Москва, Тверская 7» - один и тот же адрес, даже если сохранён у двух разных клиентов. Сравнение с Entity: две сущности с одинаковыми атрибутами - всё равно разные, если разные id.
VO иммутабельны: изменение атрибута означает создание нового VO, не модификацию существующего. Это следует из отсутствия идентичности - если у объекта нет id, «изменённый объект» и есть уже другой объект с другим значением.
Примеры: Money (amount + currency), Address, DateRange, Email, PhoneNumber, Coordinates. Использование VO вместо примитивов защищает от ошибок: нельзя случайно сложить 100 USD и 100 EUR, если Money - VO с валютой; нельзя передать произвольную строку туда, где ждут Email; нельзя перепутать дату начала и дату окончания периода, если для периода есть DateRange.
Практическое правило. Если в коде встречаются несколько параметров, всегда идущих вместе и связанных общим смыслом (amount + currency, latitude + longitude, street + city + zip) - это кандидат в VO. Без VO такие связи размазаны по сигнатурам функций и ломаются при изменениях.
Доменное событие - факт, произошедший в домене, значимый для бизнеса. SubscriptionCancelled, PaymentReceived, OrderShipped. Иммутабельны, содержат данные для обработки участниками.
Это один из самых важных паттернов DDD. На уровне реализации систем события обеспечивают отказоустойчивость и изолированность: публикующий контекст не ждёт синхронного ответа, фиксирует факт и продолжает работу. Потребители обрабатывают события в своём темпе, могут упасть и восстановиться без потери данных (если события персистируются).
События дают возможность масштабировать контексты независимо: производитель не связан с количеством и скоростью потребителей. Добавление нового потребителя - подписка на существующие события, без изменений в производителе.
Событие - контракт между публикующим и потребляющими контекстами. Формат становится stable-API, как REST-endpoint или gRPC-схема. Изменение требует либо обратной совместимости, либо согласования с потребителями.
Ключевое следствие - уменьшение runtime coupling. Два контекста, взаимодействующих через события, перестают быть runtime-зависимыми друг от друга: падение потребителя не валит производителя. Логическая связанность остаётся (потребитель ожидает конкретную семантику события), но контролируется через версионирование и явный контракт.
Хороший признак в системе: большинство межконтекстных взаимодействий идут через события. Это сигнал, что домены разделены правильно и действительно отражают независимые части бизнеса. Каждый контекст автономен, контракты явные.
Плохой признак: много взаимодействий спрятано внутри бизнес-операций - синхронные цепочки вызовов между контекстами. Это превращается в сложные взаимодействия, особенно когда контексты разделяются на сервисы и требуется graceful degradation: приходится изобретать circuit breakers, таймауты, фоллбэки на каждый вызов, потому что runtime coupling не был убран на уровне модели.
Второй плохой признак: бесконечные SAGA с компенсирующей логикой на каждый шаг. SAGA оправдана для распределённых транзакций, но когда она становится основным паттерном взаимодействия - это симптом неправильного деления на агрегаты или контексты. Попытка достичь изоляцию через компенсации на каждый чих даёт экспоненциальный рост сложности: отслеживать состояние, обрабатывать частичные падения, тестировать все пути компенсации - всё это быстро выходит из-под контроля.
Реализационные паттерны. Следующие элементы ближе к особенностям реализации: они формируют технический слой между доменной моделью и инфраструктурой. Многие напрямую поддерживаются фреймворками и языками, и их «название в DDD» часто совпадает с понятием из стека.
- Фабрика (Factory) - инкапсулирует сложную логику создания агрегата: валидация аргументов, инициализация дочерних сущностей, генерация стартовых событий. В простых случаях - обычный конструктор. В Ruby оформляется как class method
Order.create_from_cart(...); в языках с static factory methods - отдельной функцией класса. - Репозиторий (Repository) - абстракция над хранилищем агрегатов. Скрывает БД от доменного слоя: доменная логика работает с
OrderRepository.find(id), не зная, SQL это, документная БД или key-value store. В Rails ActiveRecord фактически совмещает Entity и Repository в одном классе - упрощает код для простых случаев, размывает границы в сложных. В Java/JPA репозиторий - явный интерфейс с реализацией-обёрткой над EntityManager. - Спецификация (Specification) - инкапсулирует бизнес-правило-критерий как композируемый объект. Пример:
EligibleForDiscount- спецификация, которую можно применить кCustomerдля проверки или использовать в Repository для фильтрации. Реализуется чаще как query builder поверх ORM; в функциональных языках - как композиция предикатов. - Unit of Work - отслеживает изменения во время бизнес-транзакции и координирует операции записи. Обычно предоставляется ORM (ActiveRecord transactions, JPA EntityManager, Entity Framework DbContext) без явной абстракции в доменном коде.
- Application Service - оркестрирует use case: принимает команду из UI или внешнего API, координирует работу агрегатов и доменных сервисов, возвращает результат. Тонкий слой без бизнес-логики, только координация. Отличается от Domain Service: тот - бизнес-операция на языке домена, этот - транспортно-ориентированный вход.
- Диспетчер событий - технический слой вокруг концепта доменных событий, инфраструктура publish/subscribe. В Rails типично
ActiveSupport::Notificationsилиdry-events; в Java -Spring ApplicationEventPublisher; в Python -blinker.
Эти паттерны полезно знать, но не обязательно реализовывать руками. Фреймворк обычно даёт 60-80% из коробки: ORM покрывает Repository и Unit of Work, native-конструкторы работают как Factory, встроенный dispatcher - события. Важно различать доменные паттерны (Aggregate, Entity, VO, Domain Service, Domain Event - смысл идёт от бизнеса) и реализационные (Factory, Repository, UoW, Specification, Application Service - смысл технический). Первые выбираются по бизнес-необходимости, вторые - по удобству стека.
Ограничение: тактика не обязательна. Она применяется, когда доменная сложность это оправдывает. Для CRUD-формы - избыточна. Для core-поддомена с сложными бизнес-правилами - окупается.
Как разные архитектуры реализуют DDD
DDD не предписывает одну архитектуру. Доменная модель может жить в разных структурах кода, каждая со своими trade-offs. Ниже - как ключевые элементы DDD (entity, aggregate, repository, domain service) укладываются в основные архитектурные паттерны.
Слоистая архитектура (Layered / N-Tier)
Структура: представление → прикладной слой → доменная модель → слой доступа к данным. Каждый слой зависит от нижележащего.
Как ложится DDD: агрегаты и сущности - в доменном слое; repository - на границе доменного и data access; application services - в прикладном слое; domain services - в доменном.
Проблема: доменный слой зависит от data access (вниз). Репозиторий напрямую связан с ORM или базой, что затрудняет юнит-тестирование модели без БД и замену инфраструктуры.
Когда подходит: CRUD-приложения с тонкой бизнес-логикой. Rails-приложения по умолчанию - это слоистая архитектура, где ActiveRecord совмещает data access и entity. Работает, пока логика простая.
Когда ломается: доменные правила усложняются, требуется независимое тестирование модели, нужна гибкость в замене инфраструктуры (смена ORM, БД, добавление новых источников данных).
Порты и адаптеры (Hexagonal / Onion / Clean Architecture)
Структура: доменная модель в центре. Вокруг - порты (интерфейсы, определённые доменом). Снаружи - адаптеры: БД-адаптер, HTTP-адаптер, event-bus-адаптер, реализующие порты.
Как ложится DDD: агрегаты и сущности - ядро. Repository-интерфейс - порт в домене, ORM-реализация - адаптер снаружи. Application services - на границе порта и адаптера. Domain services - в ядре.
Ключевое отличие от слоистой: зависимости инвертированы. Инфраструктура зависит от домена, не наоборот. Домен не знает, что сохраняется в PostgreSQL или MongoDB.
Три названия одной идеи: Hexagonal (Alistair Cockburn, 2005) подчёркивает симметрию портов; Onion (Jeffrey Palermo, 2008) - концентрические слои от домена к инфраструктуре; Clean (Robert Martin, 2012) - конкретные названия слоёв (entities, use cases, interface adapters, frameworks).
Когда подходит: сложный домен, долгоживущее приложение, где инфраструктура может меняться. DDD проще всего реализовать на этой архитектуре.
Цена: больше абстракций, больше кода для той же функциональности. Для простого CRUD - overkill.
CQRS (Command Query Responsibility Segregation)
Структура: две модели в одном bounded context. Модель записи (command side) - сложные агрегаты с инвариантами, обрабатывают изменения. Модель чтения (query side) - денормализованные проекции, оптимизированы под конкретные запросы.
Как ложится DDD: write-модель - классические DDD-агрегаты со всей тактикой (Aggregate, Entity, VO, Domain Service). Read-модель - простые структуры данных без бизнес-логики, возвращаются в UI напрямую.
Синхронизация: командная модель публикует доменные события, проекции слушают и обновляются. Могут быть синхронными (в той же транзакции) или асинхронными (eventually consistent).
Когда подходит: требования к чтению сильно отличаются от записи. Запись - сложная бизнес-логика на пяти связанных агрегатах; чтение - десять разных дашбордов с денормализацией из этих агрегатов. Без CQRS либо read-модели «размазываются» по write, либо read-запросы становятся дорогими join-ами.
Цена: две модели требуют поддержки, синхронизация добавляет сложность. Частая ловушка - «давайте CQRS» как самоцель; правильный триггер - доказанная разница в требованиях к записи и чтению.
Event Sourcing
Структура: состояние агрегата хранится не как снимок текущих значений, а как последовательность событий с момента создания. Текущее состояние - результат проигрывания всех событий от начала.
Как ложится DDD: естественное расширение доменных событий. Каждое изменение агрегата - событие в event store. Агрегат восстанавливается из истории; команды генерируют новые события.
Преимущества: полный аудит (история всегда сохранена), replay для пересчёта проекций, temporal queries («что было на дату X»), естественный undo/redo.
Цена: всё сложнее. Схема событий - контракт, который нельзя просто изменить (старые события никуда не денутся). Event versioning, snapshots для больших агрегатов, миграции событий, репроекции - всё это операционно нагружает команду. Debug сложнее: для понимания текущего состояния нужно проиграть события.
Когда подходит: аудит критичен (финансы, медицина, compliance), бизнес оперирует историей как первичной сущностью, temporal queries - требование продукта, не прихоть. Без бизнес-необходимости - много сложности без соразмерной выгоды.
Модульный монолит (Modular Monolith)
Структура: один deployment-артефакт, но с явными границами модулей по bounded contexts. Каждый модуль - отдельная кодовая единица с собственной доменной моделью, которая может ничего не знать о других модулях.
Как ложится DDD: bounded context = модуль. Каждый модуль использует свою архитектурную схему (слоистая, hexagonal, CQRS) в зависимости от сложности. Коммуникация между модулями - через события или публичные API модуля, не через shared database.
Когда подходит: команда 15-50 инженеров с 3-10 bounded contexts. Проще операционно, чем микросервисы; даёт явные границы, которых не даёт традиционный монолит. Подробнее в статье «Монолит vs Микросервисы».
DDD и микросервисы: распространённая ловушка
Утверждение «bounded context = микросервис» часто в презентациях. В строгом смысле это не так.
Правильная формулировка: безопасная граница для микросервиса - bounded context. Если вы решаете делить на микросервисы, делить надо по bounded contexts. Обратное не верно: bounded context не обязан быть микросервисом.
Bounded context может быть отдельным микросервисом, модулем внутри монолита, подсистемой внутри модульного монолита. Выбор определяется:
- Операционными требованиями: разные SLA, разная частота релизов? Аргумент за разделение.
- Командной структурой: разные команды с разной скоростью - аргумент за разделение.
- Зрелостью инфраструктуры: есть ли механизмы для множества сервисов? Недостаточная зрелость - против.
- Размером команды: стартап с 10 инженерами и 5 bounded contexts - чаще монолит с модулями. 150 инженеров и 20 контекстов - микросервисы или гибрид.
Подробнее в статье «Монолит vs Микросервисы».
Ловушка - начать с «нам нужны микросервисы, давайте найдём bounded contexts для деления». Обратный порядок. Сначала bounded contexts выявляются (из DDD-анализа). Потом для каждого решается - микросервис, модуль, или часть монолита.
Алгоритм внедрения DDD
Подход зависит от стартовой позиции. Три типовых сценария с разными входными условиями: greenfield с бизнес-вовлечением, brownfield с бюджетом на выправление, и ситуация без формальной поддержки бизнеса.
Научно-популярный: greenfield с бизнес-вовлечением
Идеальный вариант: новый проект или существенная перестройка с бюджетом и явной поддержкой бизнеса. Можно пройти полный цикл от моделирования до реализации.
- Event storming с бизнесом. Собрать команду (инженеры + продукт + бизнес), выписывать события предметной области - «что случается» в бизнесе. Без технических деталей.
- Группировка событий в поддомены. События кластеризуются по тематике. Каждая группа - кандидат в поддомен. Обозначить тип: core, generic, supporting.
- Единый язык для каждого потенциального контекста. Какие термины встречаются, зафиксировать значения. Пересечения - признак, что нужны bounded contexts с разным значением одного слова.
- Границы контекстов. Где граница одного контекста и начало другого. Обычно проходят там, где меняется значение термина или где событие одного контекста становится входом другого.
- Карта контекстов. Визуализировать контексты и связи. Обозначить паттерны (Customer-Supplier, ACL и т. д.).
- Тактическое моделирование для core. Агрегаты, VO, доменные сервисы. Для generic и supporting - минимум, часто CRUD.
- Отражение в архитектуре. Границы - в модулях и сервисах. Язык - в именах. ACL - где нужно.
- Отражение в командах. Каждый core-контекст - ответственность одной Stream-aligned команды. Generic - интеграция. Supporting - отдельная команда или shared capacity.
- Итерация. Модель эволюционирует с бизнесом. Карта - живой документ, обновляется при изменениях.
Event storming не обязателен - один из способов. Альтернативы: domain storytelling, user story mapping с событиями, совместные сессии с бизнесом. Важен процесс, не инструмент.
Прагматический: существующая система, нужно выправлять
Система уже выросла органически, терминология смешана, границы размыты. Есть бюджет на умеренные улучшения, но полностью переписывать нельзя. Работа идёт параллельно с продуктовой разработкой.
- Инвентаризация терминов. Собрать карту: где какое слово что значит в существующем коде, документации, разговорах с бизнесом. Список несоответствий - карта будущих исправлений. Этот шаг обычно вскрывает больше проблем, чем ожидалось.
- Выбор одного-двух core-поддоменов для старта. Не всё сразу. Те, что дают больше боли бизнесу (регрессии, блокирующие развитие) и где изменения наиболее очевидны по ценности. Остальные ждут своей очереди.
- Описание текущих bounded contexts как есть. Даже если они плохо сформированы. Карта контекстов показывает фактическое состояние, а не желаемое. Желаемое рисуется отдельным слоем «to be» и сравнивается с «as is».
- Инкрементальный рефакторинг по мере касания кода. Перед изменениями в модуле - описать его контекст, язык, ключевые агрегаты. Изменения идут по этой карте. Не переделывать код, который не трогаешь.
- Code review как контроль языка. Новые термины в PR проходят через соответствие единому языку; несоответствия возвращаются на уточнение. Это медленно, но единственный устойчивый способ удержать терминологию в растущей кодовой базе.
- Anti-Corruption Layer на границах между старым и новым кодом. Обновлённый контекст не загрязняется legacy-моделью. ACL - это цена прогресса при incremental подходе.
- Event storming только для новых фич или когда меняется core-поддомен. Не пытаться прогонять всю систему - это займёт месяцы и не даст непосредственной ценности.
Подпольный: без бизнес-buy-in, но проблемы решать надо
Ценность DDD для бизнеса доказать не удалось, формального бюджета нет, но проблемы отсутствия модели реальны: регрессии, долгий onboarding, запутанный код, страх трогать биллинг. Двигаться всё равно можно, но осторожно.
- Не называть это «DDD». В разговорах с бизнесом формулировка: «уменьшаем регрессии в биллинге», «ускоряем onboarding новых инженеров», «стабилизируем сценарий X». Продаётся результат, не термин. Слово «DDD» триггерит реакцию «инженеры опять хотят красивой архитектуры».
- Начать с одного bounded context в своей зоне влияния. Не просить разрешения на большое - делать локально там, где решения в твоей юрисдикции. Обычно это один модуль или один сервис, за который ты явно отвечаешь.
- Внутри своей зоны вводить единый язык через code review и документацию. Термины начинают жить в коде без объявления «мы делаем DDD». Новый инженер видит согласованную терминологию и начинает её использовать естественно.
- ACL между обновлённой зоной и legacy. Свой контекст изолируется от чужого словаря, не распространяет хаос и не принимает его. ACL в подпольном режиме критичен - без него прогресс размывается обратно.
- Фиксировать метрики до и после: bug rate в своей зоне, время на новую фичу, время onboarding, количество инцидентов. Нужны цифры как доказательство. Без них - аргумент «мне так спокойнее», который бизнес не купит.
- Через результаты постепенно продавать подход шире. Когда видны количественные улучшения, разговор с бизнесом переходит из «хотим потратить квартал на архитектуру» в «этот подход даёт измеримые результаты, давайте расширим на следующую зону».
Подпольный вариант не идеален, но часто единственно возможен на первых этапах. Главное ограничение - одинокий инженер не может пере-сделать систему целиком; подпольная версия работает только там, где есть хотя бы небольшая зона локального контроля и возможность показать результат.
Почему DDD последнее, что выкидывается при упрощении
Регулярно на ретроспективах или в разговорах про упрощение возникает вопрос: что можно убрать. В моём опыте DDD - последнее, что убирается.
Причина простая. Предметная область - место, где компания зарабатывает деньги. Всё остальное - стек, процессы, инструменты, конкретные архитектурные решения - сервисное. Стек меняется, процессы эволюционируют, фреймворки устаревают. Доменная модель, если правильно отражает бизнес, переживает и стек, и фреймворки.
Критично выравнивание в обе стороны:
- Бизнес → код: новые требования транслируются без потери терминологии. Когда CEO говорит про новую фичу, инженеры понимают, в каком контексте, какие сущности затрагиваются, какие события генерируются.
- Код → бизнес: инженеры понимают, что их артефакты значат для клиента. Не «мы изменили класс
User», а «мы разрешили B2B-клиентам иметь несколько администраторов с разными правами».
Когда выравнивание есть, рост не требует переизобретать модель каждые полгода. Когда его нет, начинается деградация: термины в коде расходятся с бизнесом, архитектура перестаёт отражать бизнес-зоны, команды работают над фичами, не понимая, к какой зоне те относятся. Восстановление дороже поддержания.
Что можно упростить безопасно:
- Конкретные тактические паттерны. Не каждый агрегат нужен. Не каждая зона требует сложной модели.
- Архитектурные паттерны. CQRS и event sourcing - где их окупаемость явна. По умолчанию - слоистая или hexagonal.
- Моделирование для generic и supporting. В этих зонах DDD в минимальном виде.
Что трудно убрать без последствий:
- Единый язык. Отказ - прямой путь к путанице на стыках.
- Bounded contexts. Отказ - растущий класс
Customer, неподдерживаемый через два года. - Карта контекстов. Без неё изменения делаются вслепую.
Стратегический DDD - ubiquitous language, bounded contexts, карта контекстов - выживает при любом упрощении. Тактика и конкретные архитектурные решения - кандидаты на урезание первыми.
Частые ошибки и антипаттерны
- DDD без предметной области. Тактика без участия бизнеса, без event storming, без выявления поддоменов. «DDD-стиль код» без связи с моделью. Cargo cult.
- Аналитический паралич. Год моделируем, не пишем. DDD - не водопад. Моделирование и реализация идут итеративно.
- Тактика без стратегии. Агрегаты размечены, стратегия не проведена. Команды не знают про bounded contexts, единый язык не сформирован. Декорации.
- Единая модель на весь домен. Один
Customerна весь бизнес. Через 2-3 года - 3000 строк, 50 полей, никто не понимает, какие инварианты действуют в каких сценариях. - Слишком крупный агрегат. Всё связанное помещено в один агрегат. Lock contention, невозможно параллельно обрабатывать независимые операции. Полезная эвристика (Vaughn Vernon, «Effective Aggregate Design», 2011): держать агрегат минимальным, достаточным для поддержки инварианта. Это не жёсткое требование, но нарушение почти всегда требует явного обоснования - часто крупный агрегат распадается на несколько связанных событиями.
- Event sourcing как самоцель. «Давайте все события». Без явной потребности (аудит, temporal, сложная согласованность) - усложнение без выгоды.
- DDD для CRUD. Тактика на простые справочники или админ-формы. Overkill.
- Контекст без команды-владельца. На карте есть, ответственной команды нет. Размазан между несколькими. Это уже не DDD, это хаос с диаграммой.
Источники и смежные статьи
Первоисточники
- Eric Evans. Domain-Driven Design: Tackling Complexity in the Heart of Software (2003). Исходная книга.
- Vaughn Vernon. Implementing Domain-Driven Design (2013). Практическое руководство.
- Vlad Khononov. Learning Domain-Driven Design (2021). Лёгкое введение с акцентом на стратегический дизайн.
- dddcommunity.org - сообщество и ресурсы.
- DDD Europe - конференция, записи выступлений.
Связанные темы
- Event Storming (Alberto Brandolini) - практика выявления событий.
- CQRS (Greg Young, Udi Dahan).
- Event Sourcing.
Смежные статьи блога
- Team Topologies - связь DDD с топологией команд.
- Монолит vs Микросервисы - bounded context vs microservice boundary.
- C4 Model - визуализация архитектуры, отражающей bounded contexts.
- Spec Kit - spec-driven development, где доменная модель часть спецификации для AI-агентов.
- SDLC для AI-агентов - процессная сторона трансформации.
- Инженерная стратегия - бизнес-стратегия в инженерных решениях.