Со временем большинство серьёзных проектов превращается в трудноизменяемые системы. DDD — это подход, который помогает этого избежать, привязывая структуру кода к бизнес-домену. Разберём с нуля: в чём проблема, как её решает DDD и когда это действительно нужно.
Проблема: бизнес и код говорят на разных языках
Представь типичный корпоративный проект через пару лет разработки:
- Бизнес говорит «заявка», в коде — полный разброд.
Request,Order,TicketиApplicationв разных сервисах — и все про одно и то же. - Изменение бизнес-правила ломает всё. «Добавить скидку для VIP» превращается в правку 15 файлов, потому что логика размазана по обработчикам, сервисам и SQL-запросам.
- Модули тянут друг друга. Платёжный модуль знает про склад, склад — про уведомления. Любое изменение вызывает каскад.
- Новый человек тонет. Погружение занимает месяцы, потому что нет явной структуры и единого языка.
Domain-Driven Design (DDD) — это подход, в котором структура и язык кода привязаны к бизнес-домену: к тому, как бизнес описывает свои процессы. Не к фреймворку, не к базе данных, не к API. Термин ввёл Эрик Эванс в книге «Domain-Driven Design: Tackling Complexity in the Heart of Software» (2003).
Ubiquitous Language — единый язык команды
Первое, с чего начинается DDD: разработчики и бизнес-эксперты договариваются об общем словаре. Этот словарь называется Ubiquitous Language (единый язык).
Что это значит на практике:
- термины из кода совпадают с терминами из разговоров с бизнесом;
- если бизнес говорит «заявка», класс называется
Application, а неRequestилиTicket; - если появляется новый бизнес-термин — он сразу появляется в коде.
Без единого языка возникает «перевод» между тем, что хочет бизнес, и тем, что написано в коде. Перевод — это потери: неточности, баги, потраченное время на выяснения.
Bounded Context — граница, где слова однозначны
Одно и то же слово в разных частях системы может означать разное. «Клиент» в отделе продаж — это потенциальный покупатель. «Клиент» в бухгалтерии — это плательщик с реквизитами. «Заказ» в момент оформления и «заказ» на складе — совершенно разные наборы данных.
Bounded Context (ограниченный контекст) — это явная граница, внутри которой термины имеют однозначный смысл. Каждый контекст живёт своей моделью, и это нормально: не нужно мучить одну универсальную модель «Заказа», пытаясь угодить всем.
Один контекст — одна команда, один набор терминов, один язык. На пересечениях контекстов — явные правила взаимодействия.
Aggregate — граница изменений
Внутри одного контекста данные не существуют в изоляции: объекты связаны между собой. Но если менять их как попало — нарушается целостность.
Aggregate — это кластер связанных объектов с одним корневым объектом (Aggregate Root). Все изменения идут только через корень. Это обеспечивает целостность: нельзя поменять строку заказа, не пройдя через сам заказ.
Пример: Order — корень агрегата, OrderLine — внутренний объект. Нельзя добавить строку, минуя метод Order.addLine(...). Агрегат сам следит за своими правилами.
Граница агрегата — это также граница транзакции: одна операция меняет один агрегат.
Domain Event — что произошло в домене
Когда что-то важное происходит в системе, об этом нужно сообщить другим частям. Domain Event — это факт, который уже случился в домене.
Примеры: OrderConfirmed («заказ подтверждён»), PaymentReceived («платёж принят»), ItemShipped («товар отправлен»).
Ключевое слово — «произошло». Событие описывает прошлое, а не команду на будущее. Другие контексты могут реагировать на событие, но не диктуют, как оно обрабатывается внутри источника.
События делают контексты слабосвязанными: платёжный сервис не знает про доставку, он просто публикует PaymentReceived, а доставка подписывается и реагирует.
Стратегические и тактические паттерны
DDD работает на двух уровнях:
Стратегический уровень — как разбить систему на части:
- определить Bounded Context'ы и их границы;
- нарисовать Context Map — кто с кем общается;
- выделить Core Domain (ядро бизнеса, где максимальная сложность) и вспомогательные поддомены.
Тактический уровень — как писать код внутри одного контекста:
- Aggregate и Aggregate Root;
- Domain Event;
- Value Object (объект-значение без идентичности: деньги, адрес, координаты);
- Repository (абстракция над хранилищем агрегата);
- Domain Service (логика, которая не принадлежит ни одному агрегату).
Частая ошибка — начинать сразу с тактических паттернов, не разобравшись со стратегическими. Сначала нужно понять границы и язык, потом — писать код.
Когда DDD оправдан, а когда нет
DDD — это инструмент для сложности. Там, где сложности нет, он добавит её сам.
DDD оправдан, когда:
- бизнес-логика нетривиальна и часто меняется;
- проект долгоживущий — годы, не месяцы;
- несколько команд работают над одной системой;
- домен сложный: маркетплейс, финансы, логистика, страхование;
- разработчики и бизнес-эксперты могут регулярно общаться.
DDD избыточен, когда:
- приложение — простой CRUD без существенной бизнес-логики;
- маленькая команда, один контекст, понятные требования;
- прототип, который нужно запустить быстро;
- нет доступа к бизнес-экспертам.
Хорошее правило: если вся бизнес-логика помещается в один if в обработчике запроса — DDD не нужен. Если бизнес-правила занимают сотни строк и продолжают расти — стоит задуматься.
Как начать
Если решили попробовать:
- Поговорите с бизнесом. Event Storming — хороший формат для начала: собираете команду и выписываете все события в домене.
- Зафиксируйте глоссарий. Список терминов — это ваш Ubiquitous Language. Пусть он живёт в документе и дополняется.
- Определите контексты. Нарисуйте Context Map — кто с кем общается, кто от кого зависит.
- Начните с одного контекста. Выберите Core Domain и примените тактические паттерны. Не пытайтесь переписать всё сразу.
Коротко
- DDD привязывает структуру кода к бизнес-домену, а не к фреймворку или базе данных.
- Ubiquitous Language — единый словарь разработчиков и бизнеса; термины в коде совпадают с бизнес-терминами.
- Bounded Context — явная граница, внутри которой термины однозначны; у каждого контекста своя модель.
- Aggregate — кластер объектов с корнем; все изменения через корень, одна транзакция — один агрегат.
- Domain Event — факт, который произошёл; делает контексты слабосвязанными.
- Стратегический уровень (границы, Context Map) первичен; тактические паттерны — вторичны.
- DDD оправдан при сложной и меняющейся бизнес-логике; для простого CRUD он избыточен.
Что почитать дальше
- Стратегические паттерны DDD — Bounded Context, Context Map, типы поддоменов.
- Тактические паттерны DDD — Aggregate, Entity, Value Object, Repository, Domain Service.
- Интеграционные паттерны DDD — как контексты общаются между собой.