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 (связь в процессе «резерв → оплата» — пойдёт по границе Инвентарь ↔ Заказы, пока внутрипроцессной).