ADR-0003 · Связь в процессе «резерв → оплата → билет»#
Статус: принят · 2026-09-16
Метод: D-04 v1.0 · шаблон ADR v1.1
Участники: команда разработки (4 человека)
Учебная запись: часть прогона E-01, система вымышлена. Форма и рассуждение настоящие.
Контекст#
Вход — 00-context.md. Хранилище — ADR-0001, границы — ADR-0002: модульный монолит.
Сначала пришлось решить, применим ли документ вообще. D-04 говорит: решение принимается для каждого процесса, пересекающего границы сервисов. Сервисов у нас нет — всё в одном процессе. По букве документ неприменим.
По существу — применим, и вот почему: процесс пересекает границу внешнего платёжного провайдера. За этой границей общая транзакция недостижима принципиально, и неважно, что по нашу сторону всё лежит в одном монолите. Значит условие применимости в D-04 сформулировано неверно: дело не в наших сервисах, а в наличии границы, за которой нет общей транзакции.
Процесс распадается на два, и только один попадает в пространство решений#
| Участок | Участники | Попадает ли в пространство D-04 |
|---|---|---|
| Выбор места → резерв | Инвентарь, один модуль, одна транзакция | Нет. Локальная транзакция, распределённости не возникает вовсе |
| Оплата → билет → уведомление | Заказы, Инвентарь, Уведомления, внешний провайдер | Да |
Дальше — только второй участок.
Три координатных вопроса#
| Вопрос | Ответ | Почему |
|---|---|---|
| Нужен ли инициатору ответ до завершения? | Асинхронное | Провайдер отвечает за секунды, хвост уходит в десятки [сторонний: CASE-09]. Удерживать запрос столько в окно старта продаж нельзя; ответ приходит вебхуком, пользователь видит промежуточное состояние |
| Обязана ли согласованность быть атомарной? | Итоговая — вынужденно | См. ниже: это третий случай, которого в документе нет |
| Кто отвечает за завершение процесса? | Оркестровка | Явная машина состояний платежа создан → авторизован → списан → возвращён, плюс неизвестен (CASE-09) |
Ось согласованности потребовала третьего значения#
D-04 предлагает выбор из двух: атомарная или итоговая. Здесь ни то ни другое:
- атомарность требуется — И3 запрещает списать деньги дважды за попытку, И2 запрещает аннулировать оплаченный билет;
- атомарность недостижима — деньги живут в системе провайдера, наша транзакция их не охватывает.
Записано как «итоговая», но это не то же самое, что «атомарность не нужна». Из недостижимости следуют обязательства, которых у обычной итоговой согласованности нет: состояние неизвестен обязано существовать, а ежедневная сверка с выпиской провайдера — обязательная часть системы, а не опция (CASE-09, DATA-16).
Дисквалификаторы#
| Условие | Сработало | Что из этого следует |
|---|---|---|
| Атомарность при хореографии | Нет | У нас оркестровка |
| Атомарность без явного инварианта | Неприменимо | Инвариант есть; проблема не в его отсутствии, а в недостижимости атомарности |
| Хореография при пяти и более участниках | Нет | Участников четыре, координация централизованная |
| Процесс дольше часа, состояние в коде | Да | Сам платёж идёт секунды, но состояние неизвестен живёт до суточной сверки. Состояние обязано лежать в таблице, а не в памяти процесса |
| Асинхронность без идемпотентного потребителя | Да | Вебхук провайдера приходит at-least-once. Нужна дедупликация по ключу идемпотентности попытки (NET-16, MSG-06) |
Два последних не отвергли решение — они превратились в обязательные работы.
Рассмотренные варианты#
Перечень не копируется: восемь сочетаний не зависят от нашего проекта и лежат в D-04. Снимком копируются три ответа, которые привели в ячейку, — они выше.
Ячейка: aeo — асинхронное взаимодействие, итоговая согласованность, оркестровка. В D-04 она названа Parallel Saga; название — метка сочетания координат, а не утверждение, что наши шаги идут параллельно.
Проверено, что мы не в соседних ячейках:
| Соседняя | Почему не там |
|---|---|
seo — синхронное |
Потребовало бы держать запрос пользователя всё время ответа провайдера, включая хвост в десятки секунд |
aao — атомарная |
Атомарность недостижима, а не отвергнута |
aec — хореография |
Никто не отвечал бы за то, что процесс дошёл до конца; при обязательной сверке это неприемлемо |
Решение#
aeo: асинхронно, итоговая согласованность, явный оркестратор.
Оркестратор — машина состояний платежа в модуле Заказы, состояние в таблице. Переходы разрешены явным списком; неизвестен — полноправное состояние, разрешаемое сверкой. Компенсация к неудачной оплате — освобождение резерва; компенсация к ошибочному списанию — возврат средств, а не откат транзакции.
Движок оркестрации не берём: у команды нет опыта, процесс короткий и одного вида. Своя таблица состояний здесь дешевле — но это решение обратимо и подлежит пересмотру при появлении второго-третьего длительного процесса.
Обоснование#
Из трёх координат две определились без обсуждения. Согласованность задана извне — инвариантом и невозможностью его обеспечить транзакцией. Взаимодействие задано поведением провайдера, а не нашим выбором. Выбирали мы только координацию, и здесь довод один: при обязательной сверке нужен кто-то, кто знает, где процесс сейчас; хореография это знание размазывает.
Эвристика D-04 «начинать с согласованности, а не со связи» сработала точно: она первой вывела на вопрос, который оказался главным, — и заодно на то, что документ этого случая не описывает.
Последствия#
Всё, что придётся построить дополнительно, — следствие ячейки, а не отдельные пожелания:
- Ключ идемпотентности на попытку оплаты; повтор возвращает результат первой (NET-16).
- Таблица состояний платежа с явными переходами и состоянием
неизвестен. - Ежедневная сверка с выпиской провайдера и процесс разбора расхождений. Расхождение никогда не равно нулю: доли процента операций требуют разбора
[сторонний: CASE-09], и процесс проектируется сразу. - Дедупликация вебхуков провайдера по ключу.
- Outbox для атомарной записи «в БД + событие уведомления» (MSG-07) — иначе двойная запись даст потерянные или задвоенные письма.
- Промежуточное состояние в интерфейсе: «оплата обрабатывается» — раз взаимодействие асинхронное, пользователю это видно, и продукт обязан это предусмотреть.
- Резерв не освобождается, пока платёж в состоянии
неизвестен— иначе нарушится И2. Значит срок удержания места фактически длиннее срока резерва, и это надо учесть в расчёте доступности мест.
Условия пересмотра#
- Изменился бизнес-инвариант: например, разрешили овербукинг — тогда атомарность перестаёт требоваться и половина обязательств отпадает.
- Провайдер начал отвечать синхронно и быстро с гарантией — координата взаимодействия меняется.
- Появился второй и третий длительный процесс — своя машина состояний перестаёт быть дешевле движка.
- Участников процесса стало пять и более.
- Модули разъехались в сервисы (ADR-0002 пересмотрен) — ячейка остаётся той же, но цена ошибки растёт.