Domain-Driven Design (DDD) — подход к проектированию программных систем, в котором модель предметной области и бизнес-правила становятся центром разработки. Подход был систематизирован Эриком Эвансом в книге Domain-Driven Design.
DDD не предписывает конкретный фреймворк, базу данных или архитектурный стиль. Его задача — помочь команде точно описать сложную предметную область в коде и не дать техническим деталям заслонить бизнес-смысл.
Предметная область и модель
Предметная область (domain) — часть реального мира, для которой создаётся система. В банковском приложении это могут быть счета, переводы, комиссии, лимиты и блокировки; в интернет-магазине — каталог, заказы, оплаты и доставка.
Модель предметной области — не UML-диаграмма и не схема таблиц. Это набор понятий, связей и правил, которыми пользуются специалисты бизнеса и разработчики. Например, правило «нельзя списать со счёта больше доступного остатка» должно быть выражено в доменной модели, а не случайно продублировано в HTTP-контроллере, SQL-запросе и пользовательском интерфейсе.
account.transferTo(recipient, amount);
В этом примере объект Account отвечает за связанные со счётом инварианты: допустимость перевода, доступный баланс и ограничения операции.
Единый язык
Ubiquitous Language — единый язык, на котором говорят аналитики, разработчики, тестировщики и предметные эксперты. Термины из этого языка должны встречаться в требованиях, тестах, именах классов и API.
Если бизнес использует понятие «подтверждённый заказ», то ConfirmedOrder лучше, чем несколько неэквивалентных названий вроде validated, accepted и approved. Единый язык уменьшает разрыв между обсуждением процесса и реализацией.
Строительные блоки тактического DDD
Тактические паттерны помогают выразить модель в коде. Их не требуется применять все сразу: ценность имеет только тот паттерн, который проясняет правила предметной области.
Entity
Сущность (Entity) обладает устойчивой идентичностью. Её свойства могут меняться, но она остаётся тем же объектом. Клиент с идентификатором CustomerId(145) остаётся тем же клиентом после смены имени или адреса.
Value Object
Объект-значение (Value Object) определяется своими значениями, а не идентичностью. Два экземпляра Money(100, "USD") эквивалентны, если совпадают сумма и валюта. Такие объекты обычно делают неизменяемыми. Типичные примеры: деньги, адрес, email, координаты и диапазон дат.
Aggregate и Aggregate Root
Агрегат (Aggregate) — группа объектов, которые изменяются как единое целое с точки зрения инвариантов и транзакционной согласованности. У агрегата есть единственная внешняя точка доступа — корень агрегата (Aggregate Root).
Например, Order может быть корнем агрегата, содержащего позиции заказа и скидки. Внешний код не изменяет OrderItem напрямую, а вызывает методы order.addItem(...) или order.removeItem(...). Так Order гарантирует, что после каждого изменения заказ остаётся корректным.
Граница агрегата не обязана совпадать с набором таблиц или объектным графом. Её выбирают по инвариантам: то, что должно быть согласовано в одной транзакции, обычно находится в одном агрегате.
Repository
Репозиторий предоставляет домену коллекционноподобный интерфейс для загрузки и сохранения агрегатов:
Order order = orderRepository.findById(orderId);
order.confirm();
orderRepository.save(order);
Репозиторий — это не обязательно Spring Data repository и не обязан отражать каждую таблицу. Он скрывает детали хранения, чтобы доменная логика не зависела от SQL, ORM или конкретной СУБД.
Domain Service
Доменный сервис содержит бизнес-операцию, которая неестественно принадлежит одной сущности или объекту-значению. Например, расчёт комиссии при переводе, затрагивающий несколько счетов и правил, может быть выражен отдельным сервисом. Это не то же самое, что прикладной сервис: доменный сервис содержит бизнес-логику, а прикладной обычно координирует сценарий, транзакцию и вызовы инфраструктуры.
Factory
Фабрика инкапсулирует создание сложного объекта или агрегата. Она полезна, когда для корректного создания необходимо выполнить несколько проверок, выбрать стратегию или подготовить связанные объекты — например, при оформлении кредита.
Domain Event
Доменное событие фиксирует значимый для предметной области факт, произошедший в прошлом: OrderPaid, FundsReserved, TransferCompleted. События позволяют другим частям системы реагировать на факт без прямой зависимости: начислить бонусы, отправить уведомление или обновить отчёт.
Доменное событие не тождественно сообщению в Kafka. Оно сначала является понятием модели; способ доставки — отдельное инфраструктурное решение.
Ограниченные контексты
Bounded Context — явная граница, внутри которой у терминов и модели есть одно определённое значение. Один и тот же термин может быть разным в разных контекстах: Customer в CRM — потенциальный или действующий клиент, в биллинге — плательщик, а в доставке — получатель заказа.
DDD не предлагает создавать одну универсальную сущность Customer для всей компании. Вместо этого каждый контекст получает собственную модель и собственный язык. Связи между контекстами описывают на Context Map: например, каталог передаёт данные о товарах контексту заказов, а заказы взаимодействуют с оплатами и доставкой.
Граница bounded context иногда совпадает с микросервисом, но это не правило. DDD одинаково применим и в модульном монолите; микросервисы без ясных контекстов лишь переносят путаницу в сетевые вызовы.
Когда DDD полезен
DDD наиболее полезен, когда основная сложность системы заключена в правилах, исключениях и процессах предметной области: в банковских продуктах, страховании, логистике, медицине, ERP и больших корпоративных системах.
Для простого CRUD-приложения с несколькими независимыми таблицами полный набор паттернов может быть избыточен. В таком случае достаточно ясных названий, небольшой модели и дисциплины в размещении бизнес-правил.
DDD в проектировании систем
При проектировании системы DDD предлагает сначала определить модель, а затем инфраструктуру. Для системы денежных переводов это означает выяснить:
- какие агрегаты существуют — например,
AccountиTransfer; - какие инварианты должны сохраняться — например, запрет отрицательного доступного остатка;
- где проходят границы транзакционной согласованности;
- какие доменные события возникают —
TransferInitiated,FundsReserved,TransferCompleted; - какие bounded contexts участвуют —
Accounts,Payments,Clearing.
После этого выбирают API, очередь сообщений, хранилища, механизм масштабирования и другие технологии. Главный принцип DDD состоит в том, что технологии обслуживают модель предметной области, а не определяют её.