Перейти к содержанию

ADR-0002 · Границы: модульный монолит и его модули#

Статус:    принят · 2026-09-16
Метод:     D-02 v2.1 · шаблон ADR v1.1
Участники: команда разработки (4 человека)

Учебная запись: часть прогона E-01, система вымышлена. Форма и рассуждение настоящие.

Контекст#

Вход — 00-context.md. Источник истины выбран в ADR-0001: управляемый PostgreSQL.

Семь вопросов D-02 на гринфилде отвечаются наполовину. Документ строит их вокруг истории изменений — репозитория, трекера, существующей схемы. Здесь ничего этого нет:

Вопрос Ответ
Сколько изменений за 3–6 месяцев затронуло больше одного модуля? [нет данных] — истории не существует
Какие операции обязаны выполняться атомарно? И1: резерв места. И2: подтверждение резерва билетом. [требования]
У каких сущностей больше одного пишущего? [нет данных] — схемы ещё нет
Где одно слово означает разное? Три значения у слова «заказ», см. ниже [орг]
Сколько команд, кто за что отвечает? Одна команда, четыре человека [орг]
У каких частей профиль нагрузки различается на порядок? Чтение схемы зала и запись резервов: 27× в пике [расчёт W1]
Что будет переписываться в ближайший год? [нет данных] — роадмапа за пределами запуска нет

Три из семи ответов структурно пусты, и закрыть их нечем: эвристика D-02 «посчитать связность изменений за час по трекеру» на системе, которой ещё нет, невыполнима.

Язык разошёлся уже на этапе требований — единственный вопрос, ответивший содержательно:

Где Что значит «заказ»
Афиша и каталог Позиция в продаже: событие, категория, цена
Схема зала и резервы Занятое место с номером и сроком удержания
Оплата и бухгалтерия Сумма, статус платежа, документ на пять лет хранения

Дисквалификаторы#

Сработали три, и все три отсекают не разрез, а саму идею разрезать сейчас — случай, который D-02 предусматривает явно.

Условие Как выполнено
Нет наблюдаемости, автоматического выката и трассировки Ничего из этого не существует: система не запущена. Для микросервисов это входное требование, а не следствие
У получившегося сервиса нет владельца Одна команда на все кандидаты; дежурит тот, чья очередь
Сервисов больше, чем команд, способных их содержать Любой разрез даёт от двух сервисов при одной команде

Рассмотренные варианты#

Кандидаты строились, как требует D-02, применением разных принципов к собранным фактам:

  • Разрез A — по языку: каталог · инвентарь мест · заказы и оплата · уведомления.
  • Разрез B — по профилю нагрузки: путь чтения схемы зала отдельно от всего остального.
  • Нулевой вариант: модульный монолит.

Матрица не заполнялась: после дисквалификации остался один вариант. Сравнивать было не с чем — ни один из разрезов A и B не прошёл входного условия про наблюдаемость и владельцев. Запись об этом оставлена намеренно: пустая матрица здесь означает «дисквалификаторы сделали всю работу», а не «сравнение поленились провести».

Разрез B отвергнут дважды. Помимо дисквалификаторов — по существу: 27-кратная разница чтений и записей выглядит как основание для разреза по профилю нагрузки, но путь чтения схемы зала не является ограниченным контекстом. У него нет своего языка, своих инвариантов и своего владельца данных; это проекция чужих данных для ускорения чтения. Такие вещи решаются кэшем или репликой (D-03, этап 5), а не границей сервиса.

Решение#

Модульный монолит из четырёх модулей, границы по языку, проверяются на сборке:

Модуль Владеет Не имеет права
Каталог События, категории, цены, схемы залов Знать о резервах и билетах
Инвентарь Места, резервы, их сроки Знать о платежах
Заказы Заказы, билеты, платежи Писать в таблицы мест напрямую
Уведомления Шаблоны, отправки Писать куда-либо, кроме своих таблиц

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

Обоснование#

Решение вытекло из дисквалификаторов, а не из сравнения — и это нормальный исход, а не признак недоработки.

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

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

Последствия#

  • Самая дорогая будущая граница — между Инвентарём и Заказами. Подтверждение резерва билетом (И2) — одна транзакция через эту линию. Разрез по ней превращает основной сценарий в сагу; если когда-нибудь придётся делить, эта граница режется последней и с наибольшей подготовкой. Записано здесь, потому что через год это будет неочевидно.
  • Выкатка всей системы целиком; отказ любого модуля роняет всё. При двух зонах доступности и одной команде это приемлемо, при старте продаж — предмет отдельного внимания на W7.
  • Масштабируется система целиком. Для пика 8 000 чтений/с это не проблема только потому, что путь чтения будет снят кэшем — без него решение неверно.
  • Проверка зависимостей на сборке — работа, которую надо сделать в первый месяц. Отложенная, она не делается никогда, и через полгода модули окажутся монолитом без границ.
  • Уведомления — первый кандидат на вынос, если он понадобится: свой язык, нет транзакционной связи с остальными, отказ терпим.

Условия пересмотра#

  • Появилась вторая команда-владелец. Главное условие: организационное, а не техническое.
  • Доля изменений, укладывающихся в один модуль, упала ниже 80 % — теперь это измеримо, история появится после запуска.
  • В основном сценарии появилась сага, которой при выборе не было.
  • Профиль нагрузки модулей разошёлся на порядок так, что мешает масштабированию целиком.
  • Появились наблюдаемость, трассировка и автоматический выкат — снимается первый дисквалификатор.

Связанные ADR#

Зависит от: ADR-0001. Зависимые: ADR-0003 (связь в процессе «резерв → оплата» — пойдёт по границе Инвентарь ↔ Заказы, пока внутрипроцессной).