Why DDD: bounded context
DDD отвечает на это одним договором: у сервиса есть уровни, и каждый решает ровно одну задачу. Знание идёт только снаружи внутрь: транспорт, application и база знают про домен, а домен про них — ничего. Поэтому домен не зависит ни от базы, ни от протокола вызова.
Транспорт
где нас вызывают и каким кодом отвечаем
Точка, в которой сервис вызывают: пользователь или внешняя система. Сюда приходят данные, отсюда же уходит ответ на запрос.
Решений про предметную область на этом уровне нет — транспорт переводит: protobuf, JSON или Avro в доменные сущности и обратно, через DTO, свой на каждый протокол.
Дальше DTO не едет: в application приезжает команда из доменных сущностей, а не то, что прислал пользователь. Чем команда отличается от запроса и зачем её называть типом — в главе про CQRS.
Application
какие задачи сервис умеет делать
Уровень, который определяет, какие задачи выполняет программа. Один бизнес-сценарий — это один use case со своей логикой: оформить продажу, выставить счёт, вернуть деньги.
Логика здесь — порядок шагов: достать агрегат, вызвать его, сохранить, рассказать наружу.
Правил предметной области на этом уровне нет, и он их не проверяет: application раздаёт работу домену и держит только ход операции — транзакцию, права, идемпотентность.
Агрегат
какие состояния возможны
Кластер объектов, который меняется как одно целое, и корень, через который к нему обращаются снаружи.
Смысл этой границы — инвариант: утверждение, которое обязано быть верным после каждого изменения. Сумма строк равна итогу счёта; счёт, который уже оплачен, сумму не меняет.
Инварианты проверяет сам агрегат и в одной транзакции — поэтому снаружи его нельзя оставить в состоянии, которого предметная область не допускает.
Репозиторий
как агрегат попадает в базу и возвращается оттуда
Порт объявляется в домене: взять агрегат по id, сохранить его вместе с фактами, которые он произвёл.
Реализацию пишет инфраструктура — строки, индексы, одна транзакция; строка таблицы здесь тот же DTO, только для базы.
Знание идёт снаружи внутрь: база знает про агрегат, агрегат про базу не знает.
Ограниченный контекст
чем владеем и как это называется
Граница, внутри которой у слова одно значение. Всё остальное живёт за ней и переводится на ней.
Это решение принимается раньше прочих: пока не сказано, чем контекст владеет и какими словами это называется, складывать остальные уровни не из чего.
Дальше — разбор на одном примере. Интернет-магазин: каталог, биллинг и доставка.
Скажи в нём «платёж» — и каждый отдел услышит своё.
- Продукт. Покупка прошла, пользователь увидел галочку.
- Процессинг. Деньги захолдированы, списания не было.
- Бухгалтерия. Деньги придут выпиской через два дня, а могут не прийти.
Три разных события под одним словом — и с этого места начинается разбор.
Шаг 1 из 12
C1: что такое ограниченный контекст
Схема справа — C1: система целиком, одним блоком.
Три отдела из примера — это три контекста, и у каждого своя правда про слово «платёж». Спорить, какая верная, бессмысленно: верны все три, просто в разных местах.
Ограниченный контекст — это и есть такое место: участок системы, внутри которого у слова одно значение, своя модель и своя команда, которая за них отвечает.
Границу задаёт язык. Там, где слово начинает значить другое, контекст закончился; раскладка каталогов и схема базы к этой границе отношения не имеют — их выбирают уже внутри неё.
Шаг 2 из 12
Что есть в типовом магазине
Клиент приходит в магазин, и внутри магазина три области.
- Каталог — корзина и оформление заказа.
- Биллинг — что клиент должен и что уже оплачено.
- Доставка — посылки, перевозчики и где заказ сейчас.
Три области, три команды, три словаря. Стрелка пока одна и ведёт в магазин целиком: кто внутри с кем разговаривает, выяснится позже.
Шаг 3 из 12
Core, supporting, generic
Те же три области, но с лейблами: чем каждая является для бизнеса.
- Core — то, чем магазин отличается от соседнего: каталог и биллинг. Это делают сами и вкладывают лучших людей.
- Supporting — нужно, но конкурентного преимущества не даёт: доставка.
- Generic — решённая задача, одинаковая у всех: авторизация. Клиент сперва логинится в ней и только потом идёт в магазин, а на схеме она стоит снаружи границы: её не делают — берут готовой. Своя реализация логина не даёт магазину ничего, чего не даст чужая.
Различие практическое: по нему решают, куда идут деньги и люди.
Шаг 4 из 12
Одно слово — три значения
Клиент платит, и стрелка идёт уже не в магазин целиком, а в биллинг: это он распоряжается словом «платёж».
Пока границу не провели, «платёж» из примера значил три разных события, и на встрече все кивали, оставаясь каждый при своём; в коде это расходится так: у одного
payment.completedпро галочку, у другого — про деньги на счёте.Поэтому первым файлом в каталоге биллинга появляется словарь.
Шаг 5 из 12
Что внутри словаря
Строка на слово: само слово и одно определение — то короткое предложение, которое команда готова защищать. Синонимов здесь не бывает: два слова с одним значением означают спор, который ещё не закончился.
Первой стоит таблица про три отдела: она не выбирает победителя, а называет, как каждое из трёх значений зовётся здесь.
Словарь лежит файлом рядом с кодом и правится тем же коммитом; вики живёт своей жизнью и через полгода расходится с тем, что сервис делает. Правило простое: пока слова нет здесь, его нет и в коде — сначала называем, потом пишем.
Шаг 6 из 12
C2: домены внутри контекста
Спускаемся на C2.
Контекст — не один ящик. Внутри у него домены, и у каждого своё состояние и свои правила. У биллинга их три, и все три названы словами из его словаря.
- Invoice — что клиент должен: строки, итог, состояние счёта.
- Payment — попытка получить деньги: авторизация, списание, отказ.
- Settlement — дошли ли деньги до нашего счёта и сошлось ли с выпиской.
Виды есть и у доменов: внутри core-контекста не всё core. Счёт и платёж — то, ради чего биллинг существует; сверка нужна, но преимущества не даёт.
Каталог и доставка на схеме погасли: с этого места разбор идёт про биллинг, и относительно него соседи — внешние сервисы. Что у них внутри и где они хранят своё, биллингу знать незачем.
Шаг 7 из 12
У каждого домена своя база
Состояние домена лежит в его собственной базе. Три домена — три базы, и ни одной общей.
Из этого следует главное: «не читай чужие таблицы» перестаёт быть договорённостью, которую забывают на дежурстве. Чужих таблиц просто нет рядом — счёт физически не может прочитать таблицы платежа.
Цена тоже понятна: одной транзакцией два домена уже не изменить. Значит, согласованность между ними асинхронная, и об этом следующий шаг.
Шаг 8 из 12
Шина для асинхронного взаимодействия
Здесь биллинг снова целиком, без доменов: на шине разговаривают сервисы.
Каталог не звонит биллингу — он публикует факт, что заказ оформлен. Биллинг этот факт читает и выставляет счёт. Оплаченный счёт он тоже публикует фактом, а доставка его читает и начинает собирать посылку.
Кто подписан, автор факта не знает и знать не должен. Из этого следует главное: появится второй слушатель — биллинг не изменится.
Шаг 9 из 12
Кто двигает деньги
Деньги двигает не биллинг. Он просит провайдера и ждёт ответа — это синхронный вызов, и отказ провайдера тоже ответ.
Провайдер снаружи границы: он не наш, его словарь не наш, и разговор с ним идёт через перевод. Своё состояние биллинг при этом держит сам — что попросили и что ответили, записано у него, а не только у провайдера.
Схема контекстов на этом закончена: видно, чем контекст владеет, где его состояние, кого он зовёт и кому рассказывает. Дальше — C2 самого биллинга: из каких частей он состоит внутри.
Шаг 10 из 12
C2: итоговая картина
C2 целиком: биллинг с тремя доменами и их базами, соседи по магазину, шина и внешние сервисы — авторизация и платёжный провайдер.
Шаг 11 из 12
C2: один домен и ничего кроме
Дальше остаётся только биллинг, а внутри биллинга — только счёт.
Магазин с соседями уже показан, и в кадре он больше не нужен. Платёж и сверка были нужны, чтобы показать: в контексте не один домен — внутри его границы живёт несколько сервисов, и каждый отвечает за свой фронт работ: выставить счёт, получить деньги, сойтись с выпиской. Дальше история идёт через счёт, и остальное только отвлекало бы.
Остаётся домен, его база и шина, на которую он рассказывает, что счёт оплачен. С этого кадра начинается код.
Шаг 12 из 12
Модули внутри домена
Домен — не один кусок кода. Справа дерево, и в нём видно три уровня:
billing— контекст, и словарь лежит в его корне, потому что язык один на контекст;invoice— домен внутри контекста; каталоги внутри домена — его модули. Их три.- issuing — выставить счёт: из факта «заказ оформлен» собрать строки, итог и срок оплаты.
- payment — зачесть оплату: применить деньги к счёту и рассказать наружу, что он оплачен.
- overdue — напомнить о просрочке: найти счета, у которых срок вышел.
Делит их сценарий, а не слой. Такой модуль называется вертикальным слайсом: транспорт, сценарий и работа с агрегатом лежат внутри него вместе — не «все ручки здесь, все репозитории там».
Видно это уже по входам:
issuingзовёт шина,payment— синхронный вызов процессинга,overdue— расписание. Три разных входа, и ни один из них домену не принадлежит.
Листать шаги можно стрелками ← и →.