DDD — это не про то, чтобы выучить три определения и кивать на собеседованиях. Это про то, чтобы вся команда — от аналитика до разработчика — думала о системе одинаково. А для этого мало знать слова «bounded context», «aggregate» и «ubiquitous language». Нужно увидеть, как они работают на реальном примере.
Если общая картина DDD вам ещё незнакома, загляните в обзорную статью — там разбирается, кому и зачем это нужно. А если после неё термины всё ещё путаются, есть разбор DDD простыми словами с понятными аналогиями. Здесь же мы пойдём другим путём: возьмём один сквозной домен и пройдём его полностью — от первого слова бизнеса до архитектурной схемы.
Домен — банковский кредитный конвейер. Почему он? Потому что это классический сложный домен: много участников, жёсткие правила, регулирование. Идеальный полигон для DDD.
Ubiquitous Language: когда «заявка» — это не «анкета» и не «обращение»
Первое, с чего начинается DDD в любом проекте — единый язык. Не глоссарий в Confluence, который никто не читает. А живое соглашение о том, как называются вещи.
Ubiquitous Language — это договорённость: бизнес, разработка и тестирование называют одну и ту же сущность одним и тем же словом. Всегда.
В банке с этим беда. Кредитный менеджер говорит «заявка». Бэкенд-разработчик в коде пишет CreditRequest. Аналитик в требованиях использует «обращение». Фронтендер рисует форму и вешает класс loan-application-form. Четыре названия для одного и того же — и это только начало.
Как мы строим единый язык для кредитного конвейера
Собираем ключевых участников: продакт-оунера, кредитного методолога, системного аналитика и техлида. Проводим event storming на 2–3 часа. Выписываем все события, которые происходят с кредитной заявкой от подачи до выдачи.
Вот что получается после первого раунда:
| Термин бизнеса | Код (до DDD) | Код (после DDD) | Определение |
|---|---|---|---|
| Кредитная заявка | CreditRequest | LoanApplication | Совокупность анкеты заёмщика, запрашиваемых параметров кредита и документов |
| Заёмщик | Customer | Borrower | Физическое лицо, подавшее заявку (не путать с Client — это более широкое понятие) |
| Скоринговый балл | score | ScoringResult.score | Числовая оценка кредитоспособности от 300 до 850 |
| Решение по заявке | status | UnderwritingDecision | Итог андеррайтинга: одобрено / отказано / требуется доп. проверка |
| Кредитный договор | Contract | LoanAgreement | Юридический документ с графиком платежей, ставкой и полной стоимостью кредита |
Примечательно, что половина проблем возникает не от недостатка знаний, а от того, что люди просто называют одно разными словами. После такого стола в коде появляются осмысленные имена, и главное — бизнес их узнаёт. Кредитный методолог открывает PR и видит там Borrower, LoanAgreement, ScoringResult — и ему не нужно переспрашивать аналитика, «что здесь происходит». Он и так понимает.
Bounded Context: где заканчивается одна модель и начинается другая
Теперь — самое важное. Когда единый язык зафиксирован, мы обнаруживаем, что не все термины живут в одном пространстве.
Bounded Context — это логическая граница, внутри которой термин имеет ровно одно значение. За пределами границы — другое значение. Или вообще другая модель.
В кредитном конвейере таких границ минимум пять:
Диаграмма показывает пять bounded contexts кредитного конвейера. Каждый — со своей моделью и атрибутами. Связи между контекстами — через идентификаторы (applicationId, agreementId), а не через общие объекты. Заявка (синий) входит в скоринг (жёлтый) и получает результат. Андеррайтинг (фиолетовый) выносит решение и возвращает его обратно в заявку. Одобренная заявка переходит в договор (зелёный), а договор — в выдачу.
Почему именно пять контекстов, а не три или семь? Ответ — в том, кто и как часто меняет каждый из них.
Критерии выделения контекстов
Разные команды. Скорингом занимается риск-аналитика — они калибруют модель, подключают бюро кредитных историй. Андеррайтингом — кредитные эксперты, которые принимают решения по сложным случаям. Им не нужен доступ к исходному коду скоринга, как и скорингу — к правилам андеррайтинга.
Разная частота изменений. Кредитный договор — стабильный юридический шаблон, меняется раз в квартал (если регулятор не подкинет сюрприз). Скоринговая модель перекалибровывается ежемесячно на свежих данных. Если всё это лежит в одном коде — при каждом изменении скоринговой модели вы рискуете сломать договор. Неудобно, да?
Разные бизнес-правила. Выдача кредита требует взаимодействия с банковским ядром, АБС и платёжным шлюзом — это чисто технический контекст. Кредитная заявка — это пользовательский интерфейс, валидация полей, загрузка документов. Им нечего делать вместе.
Пять контекстов — не догма. Для ипотеки, например, добавится контекст «Оценка залога» (там взаимодействие с БТИ, оценщиками, Росреестром). Для кредитных карт — контекст «Лимиты и обслуживание». Количество контекстов растёт ровно настолько, насколько усложняется домен.
Aggregate: что менять атомарно, а что — нет
Когда контексты определены, внутри каждого нужно выделить агрегаты. Aggregate — это кластер объектов, изменения в котором проходят как единая транзакция. Через корень агрегата (Aggregate Root) идёт всё взаимодействие с внешним миром.
В контексте «Кредитные заявки» главный агрегат — LoanApplication. Посмотрим на его устройство детально:
Синий блок — корень агрегата LoanApplication. Всё, что внутри — его дочерние сущности: заёмщик (фиолетовый), данные о доходах (фиолетовый), залог (жёлтый), отчёт из бюро кредитных историй (жёлтый). Серые блоки — внешние агрегаты и контексты. Связь с ними — через идентификаторы (customerId, officerId, applicationId) и через пунктирную линию (асинхронное взаимодействие).
Ключевой момент: нельзя изменить доход заёмщика напрямую, минуя LoanApplication. Код income.setMonthlyIncome(150000) — неправильно. Правильно: loanApplication.updateIncome(150000). Корень агрегата проверяет бизнес-правило (доход не может быть отрицательным, для беззалогового кредита — не ниже порога) и только потом обновляет данные.
Правила, с которыми агрегаты не превращаются в монстров
1. Агрегат — это единица консистентности, а не единица хранения. Если Borrower меняется независимо от LoanApplication (скажем, заёмщик исправил опечатку в номере паспорта отдельным запросом) — это два разных агрегата. Не надо пихать всё в одну «корзину» ради удобства.
2. Ссылки — только по идентификатору. Агрегат хранит customerId, а не объект Customer с 30 полями. Это даёт независимость загрузки и сохранения, а заодно не даёт соблазну обратиться к customer.address.street напрямую через агрегат заявки.
3. Одна транзакция — один агрегат. Если нужно изменить и LoanApplication, и LoanAgreement — это две транзакции с eventual consistency. Да, это сложнее, чем один BEGIN...COMMIT. Но это масштабируется. А один гигантский COMMIT на полбазы данных — нет.
4. Бизнес-правила живут внутри агрегата. «Кредит не может быть выдан без положительного решения андеррайтинга», «сумма займа не может превышать 5-кратный годовой доход» — эти проверки должны быть в коде агрегата, а не размазаны по сервисному слою.
Почему это работает вместе: сквозной сценарий
Соберём всё в один сценарий. Клиент подаёт заявку на потребительский кредит:
Шаг 1. Контекст «Кредитные заявки» создаёт агрегат LoanApplication со статусом DRAFT. Все поля проходят валидацию внутри агрегата: доход не ниже порога, паспортные данные заполнены, залог указан (если требуется).
Шаг 2. Агрегат отправляет applicationId в контекст «Скоринг» (через событие ApplicationSubmitted). Скоринговая модель — это другой bounded context. Она знает о заявке только applicationId и минимальный набор данных: возраст, доход, кредитную историю.
Шаг 3. Скоринговая модель возвращает ScoringResult в контекст «Андеррайтинг». Андеррайтинг — самостоятельный контекст со своими правилами: например, если скоринговый балл в «серой зоне» (550–650), заявка уходит на ручную проверку кредитному офицеру.
Шаг 4. Андеррайтинг формирует UnderwritingDecision и отправляет его обратно в агрегат LoanApplication. Статус заявки меняется: APPROVED или REJECTED. Заметьте: андеррайтинг не трогает поля заявки напрямую — он только возвращает решение через API агрегата.
Шаг 5. Для одобренной заявки контекст «Кредитный договор» формирует LoanAgreement с графиком платежей и ПСК. Это новый bounded context — он не знает деталей скоринга и андеррайтинга. Ему нужны только утверждённые параметры: сумма, ставка, срок.
Шаг 6. Контекст «Выдача» получает agreementId, связывается с АБС и платёжным шлюзом, переводит деньги на счёт клиента. Для контекста выдачи не существует понятия «скоринговый балл» или «андеррайтинг» — только договор и платёжное поручение.
Весь этот сценарий держится на трёх китах: единый язык не даёт запутаться в терминах, bounded contexts изолируют зоны ответственности, агрегаты защищают консистентность данных.
Типичные ошибки: что ломается, если DDD внедрить «как-нибудь»
Ошибка 1: Ubiquitous Language в виде PDF-файла на общем диске
«Мы внедрили DDD: вот документ на 40 страниц со всеми терминами». Через месяц документ устарел, через два — никто не помнит, где он лежит. Единый язык живёт в коде. Название класса в Java/C#/Python — это и есть глоссарий. Обновили термин — переименовали класс. А Confluence — только для справки.
Ошибка 2: «Скоринг и андеррайтинг — одно и то же, объединим»
Первое желание junior-архитектора: «а давайте скоринг и андеррайтинг в один контекст, они же рядом». Соблазнительно. Но скоринг — это математическая модель с ежемесячными перекалибровками. Андеррайтинг — это бизнес-правила, которые диктует кредитный комитет. Объединили — и теперь при каждом изменении скоринговой модели вы пересобираете и деплоите андеррайтинг. А он не менялся. Вы создали зависимость там, где её не должно быть.
Ошибка 3: Агрегат-гигант
«Пусть LoanApplication включает в себя всё: и Borrower, и LoanAgreement, и PaymentSchedule, и Disbursement — удобно же, один findById() и всё готово». Через полгода загрузка одной заявки подтягивает 40 таблиц и блокирует их на запись. При 100 заявках в минуту база данных говорит «спасибо, я устала». Держите агрегаты маленькими.
Ошибка 4: Забыть, что DDD — только для Core Domain
Кредитный конвейер — Core Domain банка. Здесь DDD оправдан. Но админка для управления справочником отделений? Или внутренний портал для сотрудников? Там CRUD достаточно. Не надо внедрять bounded contexts в лендинг-страницу. Это не серебряная пуля.
Когда DDD окупается, а когда — нет
| Критерий | DDD нужен | DDD избыточен |
|---|---|---|
| Сложность домена | Много правил, исключений, регуляторных требований | Простой CRUD |
| Размер команды | 4+ человек, разные роли | 1–2 разработчика |
| Частота изменений | Домен меняется постоянно (законы, рынок) | Стабильные требования на годы |
| Срок жизни проекта | Годы, десятки релизов | Прототип, MVP на пару месяцев |
| Цена ошибки | Высокая: штрафы, репутация, деньги клиентов | Низкая: можно переписать |
Банковский кредитный конвейер по всем пунктам попадает в левую колонку. Цена ошибки — реальные деньги клиентов (и, что немаловажно, штрафы от ЦБ). Команда — десятки человек. Требования меняются раз в квартал, а то и чаще. Это классический Core Domain, для которого DDD создавался.
Заключение
DDD — это не про «надо рисовать bounded contexts для всего». Это про то, чтобы сложный домен не превратился в запутанный код, который никто не решается трогать.
Три концепции, которые мы разобрали, работают как слои: единый язык — фундамент, без которого все договорённости рушатся. Bounded contexts — стены, которые не дают изменениям в одной части системы ломать другую. Агрегаты — замки на дверях, через которые данные не меняются в обход правил.
В банковском кредитном конвейере это выглядит так: пять контекстов (заявки, скоринг, андеррайтинг, договор, выдача) живут своей жизнью, агрегат LoanApplication держит консистентность заявки, а единый язык не даёт кредитному менеджеру и разработчику говорить на разных языках.
Начните с первого шага: договоритесь с командой о десяти ключевых терминах вашего домена. И запишите их прямо в коде, именами классов. Если через месяц эти имена всё ещё актуальны и никто не создал класс с названием DataProcessor — вы на верном пути.
P.S. Если после прочтения вы подумали «это всё очевидно» — отлично. Значит, вы уже интуитивно применяете DDD, просто не называли это так. А если подумали «у нас в проекте всё наоборот» — что ж, теперь вы знаете, с чего начать рефакторинг. И нет, «потом перепишем» — не вариант.