Why DDD: bounded context

ПроблемаКаждый сервис устроен по-своему: своя раскладка каталогов, путаница в терминах, переизобретение велосипеда и наступление на одни и те же грабли в сотый раз. Знание про один сервис не переносится на следующий — ни у автора, ни на ревью, ни у того, кто придёт через год.

DDD отвечает на это одним договором: у сервиса есть уровни, и каждый решает ровно одну задачу. Знание идёт только снаружи внутрь: транспорт, application и база знают про домен, а домен про них — ничего. Поэтому домен не зависит ни от базы, ни от протокола вызова.

Транспортгде нас вызывают и каким кодом отвечаем1Applicationкакие задачи сервис умеет делать2Агрегаткакие состояния возможны3Репозиторийкак агрегат попадает в базу и возвращается оттуда4Ограниченный контекстчем владеем и как это называется5
  1. Транспорт

    где нас вызывают и каким кодом отвечаем

    Точка, в которой сервис вызывают: пользователь или внешняя система. Сюда приходят данные, отсюда же уходит ответ на запрос.

    Решений про предметную область на этом уровне нет — транспорт переводит: protobuf, JSON или Avro в доменные сущности и обратно, через DTO, свой на каждый протокол.

    Дальше DTO не едет: в application приезжает команда из доменных сущностей, а не то, что прислал пользователь. Чем команда отличается от запроса и зачем её называть типом — в главе про CQRS.

  2. Application

    какие задачи сервис умеет делать

    Уровень, который определяет, какие задачи выполняет программа. Один бизнес-сценарий — это один use case со своей логикой: оформить продажу, выставить счёт, вернуть деньги.

    Логика здесь — порядок шагов: достать агрегат, вызвать его, сохранить, рассказать наружу.

    Правил предметной области на этом уровне нет, и он их не проверяет: application раздаёт работу домену и держит только ход операции — транзакцию, права, идемпотентность.

  3. Агрегат

    какие состояния возможны

    Кластер объектов, который меняется как одно целое, и корень, через который к нему обращаются снаружи.

    Смысл этой границы — инвариант: утверждение, которое обязано быть верным после каждого изменения. Сумма строк равна итогу счёта; счёт, который уже оплачен, сумму не меняет.

    Инварианты проверяет сам агрегат и в одной транзакции — поэтому снаружи его нельзя оставить в состоянии, которого предметная область не допускает.

  4. Репозиторий

    как агрегат попадает в базу и возвращается оттуда

    Порт объявляется в домене: взять агрегат по id, сохранить его вместе с фактами, которые он произвёл.

    Реализацию пишет инфраструктура — строки, индексы, одна транзакция; строка таблицы здесь тот же DTO, только для базы.

    Знание идёт снаружи внутрь: база знает про агрегат, агрегат про базу не знает.

  5. Ограниченный контекст

    чем владеем и как это называется

    Граница, внутри которой у слова одно значение. Всё остальное живёт за ней и переводится на ней.

    Это решение принимается раньше прочих: пока не сказано, чем контекст владеет и какими словами это называется, складывать остальные уровни не из чего.

Дальше — разбор на одном примере. Интернет-магазин: каталог, биллинг и доставка.

Скажи в нём «платёж» — и каждый отдел услышит своё.

  • Продукт. Покупка прошла, пользователь увидел галочку.
  • Процессинг. Деньги захолдированы, списания не было.
  • Бухгалтерия. Деньги придут выпиской через два дня, а могут не прийти.

Три разных события под одним словом — и с этого места начинается разбор.

  1. Шаг 1 из 12

    C1: что такое ограниченный контекст

    Схема справа — C1: система целиком, одним блоком.

    Три отдела из примера — это три контекста, и у каждого своя правда про слово «платёж». Спорить, какая верная, бессмысленно: верны все три, просто в разных местах.

    Ограниченный контекст — это и есть такое место: участок системы, внутри которого у слова одно значение, своя модель и своя команда, которая за них отвечает.

    Границу задаёт язык. Там, где слово начинает значить другое, контекст закончился; раскладка каталогов и схема базы к этой границе отношения не имеют — их выбирают уже внутри неё.

  2. Шаг 2 из 12

    Что есть в типовом магазине

    Клиент приходит в магазин, и внутри магазина три области.

    • Каталог — корзина и оформление заказа.
    • Биллинг — что клиент должен и что уже оплачено.
    • Доставка — посылки, перевозчики и где заказ сейчас.

    Три области, три команды, три словаря. Стрелка пока одна и ведёт в магазин целиком: кто внутри с кем разговаривает, выяснится позже.

  3. Шаг 3 из 12

    Core, supporting, generic

    Те же три области, но с лейблами: чем каждая является для бизнеса.

    • Core — то, чем магазин отличается от соседнего: каталог и биллинг. Это делают сами и вкладывают лучших людей.
    • Supporting — нужно, но конкурентного преимущества не даёт: доставка.
    • Generic — решённая задача, одинаковая у всех: авторизация. Клиент сперва логинится в ней и только потом идёт в магазин, а на схеме она стоит снаружи границы: её не делают — берут готовой. Своя реализация логина не даёт магазину ничего, чего не даст чужая.

    Различие практическое: по нему решают, куда идут деньги и люди.

  4. Шаг 4 из 12

    Одно слово — три значения

    Клиент платит, и стрелка идёт уже не в магазин целиком, а в биллинг: это он распоряжается словом «платёж».

    Пока границу не провели, «платёж» из примера значил три разных события, и на встрече все кивали, оставаясь каждый при своём; в коде это расходится так: у одного payment.completed про галочку, у другого — про деньги на счёте.

    Поэтому первым файлом в каталоге биллинга появляется словарь.

  5. Шаг 5 из 12

    Что внутри словаря

    Строка на слово: само слово и одно определение — то короткое предложение, которое команда готова защищать. Синонимов здесь не бывает: два слова с одним значением означают спор, который ещё не закончился.

    Первой стоит таблица про три отдела: она не выбирает победителя, а называет, как каждое из трёх значений зовётся здесь.

    Словарь лежит файлом рядом с кодом и правится тем же коммитом; вики живёт своей жизнью и через полгода расходится с тем, что сервис делает. Правило простое: пока слова нет здесь, его нет и в коде — сначала называем, потом пишем.

  6. Шаг 6 из 12

    C2: домены внутри контекста

    Спускаемся на C2.

    Контекст — не один ящик. Внутри у него домены, и у каждого своё состояние и свои правила. У биллинга их три, и все три названы словами из его словаря.

    • Invoice — что клиент должен: строки, итог, состояние счёта.
    • Payment — попытка получить деньги: авторизация, списание, отказ.
    • Settlement — дошли ли деньги до нашего счёта и сошлось ли с выпиской.

    Виды есть и у доменов: внутри core-контекста не всё core. Счёт и платёж — то, ради чего биллинг существует; сверка нужна, но преимущества не даёт.

    Каталог и доставка на схеме погасли: с этого места разбор идёт про биллинг, и относительно него соседи — внешние сервисы. Что у них внутри и где они хранят своё, биллингу знать незачем.

  7. Шаг 7 из 12

    У каждого домена своя база

    Состояние домена лежит в его собственной базе. Три домена — три базы, и ни одной общей.

    Из этого следует главное: «не читай чужие таблицы» перестаёт быть договорённостью, которую забывают на дежурстве. Чужих таблиц просто нет рядом — счёт физически не может прочитать таблицы платежа.

    Цена тоже понятна: одной транзакцией два домена уже не изменить. Значит, согласованность между ними асинхронная, и об этом следующий шаг.

  8. Шаг 8 из 12

    Шина для асинхронного взаимодействия

    Здесь биллинг снова целиком, без доменов: на шине разговаривают сервисы.

    Каталог не звонит биллингу — он публикует факт, что заказ оформлен. Биллинг этот факт читает и выставляет счёт. Оплаченный счёт он тоже публикует фактом, а доставка его читает и начинает собирать посылку.

    Кто подписан, автор факта не знает и знать не должен. Из этого следует главное: появится второй слушатель — биллинг не изменится.

  9. Шаг 9 из 12

    Кто двигает деньги

    Деньги двигает не биллинг. Он просит провайдера и ждёт ответа — это синхронный вызов, и отказ провайдера тоже ответ.

    Провайдер снаружи границы: он не наш, его словарь не наш, и разговор с ним идёт через перевод. Своё состояние биллинг при этом держит сам — что попросили и что ответили, записано у него, а не только у провайдера.

    Схема контекстов на этом закончена: видно, чем контекст владеет, где его состояние, кого он зовёт и кому рассказывает. Дальше — C2 самого биллинга: из каких частей он состоит внутри.

  10. Шаг 10 из 12

    C2: итоговая картина

    C2 целиком: биллинг с тремя доменами и их базами, соседи по магазину, шина и внешние сервисы — авторизация и платёжный провайдер.

  11. Шаг 11 из 12

    C2: один домен и ничего кроме

    Дальше остаётся только биллинг, а внутри биллинга — только счёт.

    Магазин с соседями уже показан, и в кадре он больше не нужен. Платёж и сверка были нужны, чтобы показать: в контексте не один домен — внутри его границы живёт несколько сервисов, и каждый отвечает за свой фронт работ: выставить счёт, получить деньги, сойтись с выпиской. Дальше история идёт через счёт, и остальное только отвлекало бы.

    Остаётся домен, его база и шина, на которую он рассказывает, что счёт оплачен. С этого кадра начинается код.

  12. Шаг 12 из 12

    Модули внутри домена

    Домен — не один кусок кода. Справа дерево, и в нём видно три уровня: billing — контекст, и словарь лежит в его корне, потому что язык один на контекст; invoice — домен внутри контекста; каталоги внутри домена — его модули. Их три.

    • issuing — выставить счёт: из факта «заказ оформлен» собрать строки, итог и срок оплаты.
    • payment — зачесть оплату: применить деньги к счёту и рассказать наружу, что он оплачен.
    • overdue — напомнить о просрочке: найти счета, у которых срок вышел.

    Делит их сценарий, а не слой. Такой модуль называется вертикальным слайсом: транспорт, сценарий и работа с агрегатом лежат внутри него вместе — не «все ручки здесь, все репозитории там».

    Видно это уже по входам: issuing зовёт шина, payment — синхронный вызов процессинга, overdue — расписание. Три разных входа, и ни один из них домену не принадлежит.

Листать шаги можно стрелками ← и →.