19 апреля 2026
Domain-Driven Design помогает связать модель предметной области с кодом. Стратегический дизайн уточняет термины, границы моделей и их связи; тактические паттерны помогают реализовать эти решения. В статье разбираю обе части и их применение в существующей системе. Для работы с агентами особенно полезны записанные термины и бизнес-правила: по ним можно уточнять задачу и проверять реализацию.
Что такое DDD и чего он не делает
Domain-Driven Design предлагает строить модель вместе с людьми, которые понимают предметную область. Термины и правила бизнеса отражаются в коде, а границы контекстов помогают разделять модели. Как эти границы соотносятся с модулями и командами, зависит от устройства продукта.
DDD состоит из двух частей:
- Стратегический дизайн - деление предметной области на поддомены, установление ограниченных контекстов, формирование единого языка, карта контекстов. Работа на уровне «где компания зарабатывает и как устроены бизнес-зоны».
- Тактический дизайн - агрегаты, объекты-значения, сущности, доменные события, фабрики, репозитории. Работа на уровне «как реализовать модель в коде».
Распространенное заблуждение: DDD = тактика. Команда читает Evans, узнает про агрегаты и репозитории, начинает размечать классы. В коде появляются знакомые паттерны, но команда не договаривается о терминах и границах модели.
Тактика работает только поверх стратегии. Без стратегии агрегаты - просто классы с методами, репозитории - обертки над ORM, единый язык - глоссарий, который никто не читает.
Для простых CRUD-задач полное доменное моделирование может оказаться избыточным. Объем моделирования выбирают по сложности правил и цене ошибок. DDD не заменяет бизнес-стратегию и не требует микросервисной архитектуры.
Поддомены и конкурентное преимущество
Предметная область компании не единое целое. Это набор зон. В каких-то зонах бизнес зарабатывает, в каких-то - поддерживает инфраструктуру работы. DDD разделяет на три типа.
Основной поддомен (core) связан с конкурентным преимуществом компании. Например, это может быть скоринг в финтехе или подбор предложений на маркетплейсе. Какая часть действительно относится к core, нужно определять для конкретного бизнеса. Здесь особенно важны доменная экспертиза и возможность менять реализацию по мере изменения задач.
Универсальный поддомен (generic) - индустриальный стандарт. Зона с зрелыми готовыми решениями. Аутентификация, биллинг через Stripe, email через SendGrid, CMS. Собственная реализация generic-поддомена требует оценки затрат: даже если напишете собственную платежную платформу, в нее придется заложить все, что Stripe уже сделал, плюс поддерживать десятилетиями.
Вспомогательный поддомен (supporting) - нужен для работы бизнеса, но не дает преимущества. Админ-панели, внутренние инструменты, системы отчетности, HR. Кандидаты на: готовые продукты, аутсорсинг или CRUD-приложения с минимальными усилиями внутри.
Практически - для каждой зоны ответить:
- Это core, generic или supporting?
- Если core: в чем преимущество, где собственная экспертиза выигрывает?
- Если generic: какое готовое решение, как подключаем?
- Если supporting: какой минимум нужен, как дешевле всего закрыть?
Core получает сильных инженеров. Generic закрывается интеграцией. Supporting закрывается минимальными средствами.
Единый язык (Ubiquitous Language)
Единый язык формируют разработчики и эксперты предметной области внутри bounded context. В разных контекстах одинаковое слово может обозначать разные модели. Поэтому нужно явно указывать, в каком контексте используется термин, и отражать эту договоренность в коде и документации.
Без единого языка: бизнес говорит «клиент», продукт говорит «пользователь», бэкенд называет 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 команда владеет контекстом целиком: язык, модель, код, эксплуатация. Соответствие не обязано быть 1:1 - одна команда часто владеет несколькими небольшими контекстами, а крупному контексту может понадобиться несколько команд. Подробнее в статье «Team Topologies».
Архитектура - последняя остановка. Границы контекстов становятся границами модулей и сервисов. Язык - именами в коде и API. Способы взаимодействия - интеграционными паттернами. Как рабочая эвристика архитектура следует из предметной области, а не выбирается отдельно от нее. На практике связь двусторонняя: архитектура и стратегия влияют друг на друга итеративно, и существующие технические ограничения (унаследованные системы, регуляторные требования, задержки) иногда диктуют выбор независимо от домена.
Порядок важен. Если начать с архитектуры («нам нужны микросервисы»), получается технически красивая система, плохо отражающая бизнес. Если начать с команд («наймем N инженеров, распределим по фичам»), команды привязаны к случайным границам. Если начать с бизнес-стратегии, но не пройти DDD-шаг, переход к командам и архитектуре идет на ощупь.
Моделирование требует времени. Его пользу можно оценивать по тому, насколько согласованная модель упрощает дальнейшие изменения в коде и работу команд.
Перевод DDD в архитектуру
Стратегический дизайн остается теорией без перевода в код. Что обязательно сохраняется:
Границы контекстов. Реализация должна сохранять понятные границы моделей и публичные способы взаимодействия. Bounded context не обязан совпадать с отдельным сервисом: в одном приложении могут находиться несколько контекстов.
Единый язык → имена в коде, API, БД. Термин Subscription в модели - это класс Subscription в коде, endpoint /subscriptions, таблица subscriptions. Нет перевода. Нет SubscriptionDTO vs SubscriptionModel с разным смыслом. Нет БД, где это customer_plans, а в коде - Subscription.
Anti-Corruption Layer. Слой перевода полезен, когда внешняя модель не должна определять внутреннюю. Его необходимость зависит от отношений между контекстами: DDD также описывает Partnership, Conformist и другие варианты взаимодействия.
Доменные события. Событие описывает факт в предметной области. Его можно использовать для связи между контекстами, но DDD не требует заменять событиями все вызовы. Синхронное API и события выбирают по требованиям к ответу, согласованности и обработке сбоев.
Частый сбой: модель в документации одна, в коде другая. Команда обсуждает «клиент с подпиской», код оперирует User и Account. Новый инженер читает код, строит у себя модель User + Account, начинает разговаривать в этих терминах. Через полгода все говорят двумя разными языками в зависимости от собеседника. Восстановление в этой точке дороже, чем поддержание с начала.
Защитить терминологию помогает дисциплина в code review. PR, в котором появляется имя, не соответствующее единому языку, - повод для обсуждения. Либо имя приводится к языку, либо язык расширяется сознательно.
DDD и AI-агенты: почему домен должен быть явным
Агент использует доступные ему код, документацию и инструменты. Устные договоренности и знания участников не попадут в его контекст автоматически; важные для задачи решения нужно передать явно.
Если в задаче неясно, что означает «клиент», агент может спросить об этом или сделать предположение. Процесс работы должен предусматривать уточнения и проверку предположений человеком. Описание контекста и терминов уменьшает неоднозначность, но не гарантирует правильную реализацию.
Поэтому терминология и бизнес-операции теперь - часть спецификации. Без DDD спека выглядит как «сделай что-то с клиентом, чтобы получилось X». Агент выбирает собственную модель «клиента» - обычно ту, что чаще встречается в обучающих данных, или ту, что кажется подходящей по контексту файла. Получается работающий код, не соответствующий бизнес-модели.
С DDD спека дает семантику. Bounded context фиксирует, в какой части домена работаем. Ubiquitous language определяет термины. Инварианты агрегата сообщают, какие правила нарушать нельзя. События описывают, что происходит в других контекстах при этом изменении. Агент следует семантике, не изобретает.
Spec Kit разделяет описание требований и план реализации. Это можно использовать для фиксации терминов и контекста задачи. Такое разделение не делает DDD обязательным условием использования инструмента.
Тактические паттерны
Инструменты реализации модели в коде. Работают только поверх стратегии.
Сущность (Entity) - отражение объекта реального мира, имеющего идентичность, которая сохраняется через время. Идентичность позволяет отличить один экземпляр от другого: два заказа с одинаковой суммой и одинаковыми позициями - все равно разные заказы, если у них разные номера. Две сущности с одинаковыми атрибутами - разные, если разные id.
Сущность живет внутри одного bounded context и работает только для него. Один бизнес-объект может существовать в нескольких контекстах с общей идентичностью, но разным содержанием. Заказ с номером ORDER-12345:
- Для бухгалтерии - сумма, состояние оплаты, налоговые атрибуты, выписанные документы.
- Для сотрудника склада - какие позиции упакованы, какие в процессе, какие ждут поступления.
- Для службы доставки - адрес, статус отправки, трек-номер.
Общий номер связывает контексты, но внутри каждого Order - разная сущность со своими атрибутами и поведением. Попытка вместить все атрибуты в один класс - путь к тысячестроковому Order, непонятному никому. Разделение на bounded contexts решает это явно: общий id связывает, модели различаются.
Агрегат - граница согласованности и транзакционной целостности, сформированная из одной или нескольких сущностей вместе с их объектами-значениями. В простых случаях агрегат и сущность совпадают один к одному: сущность User - это и есть агрегат User. В сложных - агрегат составной: корневая сущность плюс набор дочерних, которые меняются только вместе с корневой. Пример агрегата с несколькими сущностями: Order с коллекцией OrderLine - позиции нельзя менять независимо от заказа, иначе ломаются инварианты (суммы, допустимые комбинации, состояние оплаты).
Ключевое свойство: сущности внутри агрегата изменяются только через корневую сущность и только в составе транзакции над агрегатом целиком. Это делает границу агрегата явной зоной согласованности: внутри инварианты обеспечиваются кодом, снаружи - другой агрегат с собственными правилами.
Границы агрегата помогают определить, какие инварианты нужно сохранять при изменениях. Если несколько процессов меняют один агрегат, нужно выбрать способ управления конкуренцией, например оптимистичную или пессимистичную блокировку. Выбор зависит от частоты конфликтов, хранилища и стоимости повторной операции. Изменения между агрегатами требуют отдельного решения о согласованности.
Доменный сервис выражает операцию предметной области, которую неудобно разместить в сущности или объекте-значении. Название и интерфейс должны соответствовать языку контекста. Это не требование создавать отдельный класс для каждой пользовательской операции.
Общее поведение стоит выделять там, где совпадает бизнес-смысл. Различия между контекстами и сценариями не нужно скрывать в одном сервисе только ради повторного использования кода.
При этом разные бизнес-операции не должны схлопываться в одну, не отражающую бизнес-смысл. Антипаттерн - сервис UpdateOrder, одновременно отвечающий за обновление позиций в заказе, прием платежа, обработку доставки на склад. Каждая из этих операций - отдельная бизнес-сущность со своими инвариантами, событиями и, возможно, разными командами-владельцами. Слияние в один сервис теряет семантику и ломает выравнивание с языком бизнеса.
Пример: перевод денег между двумя аккаунтами - TransferMoney domain service. Он оркестрирует два агрегата (списание с одного, начисление на другой), обеспечивает согласованность, публикует событие. Сервис отражает отдельную бизнес-операцию, которая затрагивает несколько агрегатов.
Объект-значение (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-зависимыми друг от друга: падение потребителя не валит производителя. Логическая связанность остается (потребитель ожидает конкретную семантику события), но контролируется через версионирование и явный контракт.
Хороший признак в системе: большинство межконтекстных взаимодействий идут через события. Это сигнал, что домены разделены правильно и действительно отражают независимые части бизнеса. Каждый контекст автономен, контракты явные.
Плохой признак: много взаимодействий спрятано внутри бизнес-операций - синхронные цепочки вызовов между контекстами. Это превращается в сложные взаимодействия, особенно когда контексты разделяются на сервисы и требуется 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 не предписывает одну архитектуру. Доменная модель может жить в разных структурах кода, каждая со своими компромиссы. Ниже - как ключевые элементы 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. Агрегат восстанавливается из истории; команды генерируют новые события.
Преимущества: История событий может использоваться для аудита, пересчета проекций и восстановления состояния на выбранный момент. Возможность отменять операции нужно проектировать отдельно с учетом правил предметной области.
Цена: все сложнее. Схема событий - контракт, который нельзя просто изменить (старые события никуда не денутся). 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 команды; в нашем случае это рабочий default, но команда может владеть и несколькими небольшими контекстами. 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 полей, никто не понимает, какие инварианты действуют в каких сценариях. - Слишком крупный агрегат. Все связанное помещено в один агрегат. Конкуренция за блокировки мешает параллельной обработке операций. Полезная эвристика (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-агентов - процессная сторона трансформации.
- Инженерная стратегия - бизнес-стратегия в инженерных решениях.
- Martin Fowler: Bounded Context
- Martin Fowler: Ubiquitous Language