{ "index": 119, "slug": "editorial-2024-09-mechanism-adr-decisions", "title": "ADR как журнал компромисса: решение, цена и путь назад", "excerpt": "Хороший ADR сохраняет не только выбранный вариант, но и исходное ограничение, отклонённые альтернативы, цену решения и сигнал для пересмотра. Разбираем это на сценарии долгого экспорта.", "contentHtml": "

После ревью в репозитории остаётся новый endpoint, очередь или дополнительное хранилище, но исчезает причина выбора. Через несколько месяцев команда видит только реализацию: один caller ждёт ответ, другой опрашивает status, третий уже использует внутреннее поле как публичный контракт. Вопрос «почему здесь так?» снова уходит в поиск по чатам и памяти участников.

\n

Ошибка не обязательно в самом техническом выборе. Дорого обходится потеря его границ: неизвестно, какое ограничение считалось обязательным, какие варианты сравнивали, кто владеет состоянием и что делать после изменения исходных условий. ADR — Architectural Decision Record, запись архитектурного решения — нужен именно для этой причинной связи. Он не делает решение правильным навсегда и не заменяет тестирование, но оставляет проверяемую историю компромисса.

\n

Тезис статьи: ADR готов, когда независимый читатель может восстановить проблему, обязательное условие, выбранный путь, его отрицательные последствия и сигнал пересмотра. Слова «быстрее», «проще» и «надёжнее» становятся полезными только после уточнения: для какого участника, при каком отказе и каким артефактом это проверяется.

\n

Что именно фиксирует ADR

\n

Официальное сообщество ADR определяет architectural decision как обоснованный выбор, значимый для архитектуры, а ADR — как запись одного такого выбора и его rationale, то есть причины. Коллекция записей образует decision log. Поэтому один ADR отвечает на один связный вопрос, а не пытается стать полной технической документацией системы.

\n

В шаблоне Michael Nygard минимальная форма состоит из Status, Context, Decision и Consequences. В MADR форма расширяется полями decision drivers, considered options, outcome и confirmation. Это полезные шаблоны, а не универсальный закон: команда может добавить владельца, ссылки на контракт, confidence или условие пересмотра.

\n
Граница ответственности ADR и соседних артефактов
АртефактГлавный вопросЧто должно остаться внутриЧего он не доказывает
ADRПочему выбран этот вариант?Контекст, ограничения, альтернативы, решение и последствияЧто код уже корректен во всех средах
Design documentКак устроить решение целиком?Компоненты, потоки, контракты и детали реализацииЧто выбранный дизайн принят как долгосрочное решение
TestКакое поведение воспроизводится?Условия, входы, ожидаемый результат, регрессияПочему выбрана именно эта архитектура
RunbookЧто делать при операционном событии?Команды, роли, stop condition и восстановлениеЧто исходный компромисс всё ещё применим
\n

Ссылка на ticket или commit полезна для навигации, но не заменяет rationale. И наоборот, ADR не должен превращаться в копию implementation guide. Если решение требует детальной схемы, threat model или плана миграции, запишите выбор в ADR и свяжите его с отдельным документом.

\n

Сначала зафиксируйте симптом и границу

\n

Начинайте с наблюдаемого симптома, а не с названия технологии. Например: «клиент вызывает экспорт, но операция иногда не укладывается в границу HTTP-запроса; при повторе непонятно, создан ли второй экспорт». Это факт и вопрос. Гипотеза «нам нужна очередь» появляется только после проверки длительности операции, повторяемости запроса и требований caller.

\n

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

\n

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

\n
Проблема: export иногда дольше жизни HTTP-запроса.\nОбязательные условия:\n- клиент получает подтверждение создания операции;\n- повтор запроса не создаёт второй экспорт;\n- состояние имеет срок хранения и владельца очистки.\nЦена ошибки: дубль работы или запись, которую никто не удаляет.\nВопрос решения: где хранить status и кто отвечает за его переходы?
\n

Фрагмент — учебная заготовка. Он не сообщает реальную длительность, нагрузку или процент ошибок. В рабочей записи эти утверждения должны ссылаться на контракт, trace, тест, incident или другой доступный источник. Если данных нет, оставьте evidence gap и действие по его закрытию.

\n

Сравнивайте альтернативы одной меркой

\n

Сравнение ломается, когда выбранный вариант описан конкретно, а остальные получают ярлык «сложно». Приведите варианты к одному уровню и задайте одинаковые вопросы: выполняет ли вариант обязательные условия, где хранится состояние, как работает повтор, кто поддерживает очистку, как выглядит возврат и какое evidence уже есть.

\n
Учебное сравнение способов выполнить долгий экспорт
ВариантЧто закрываетЦена и рискПуть назад
Синхронный ответПростая модель для короткой операцииНе выдерживает временную границу; повтор может повторить работуНе требуется отдельное состояние, но граница остаётся нерешённой
Очередь и status у сервиса экспортаОтделяет подтверждение создания от завершения работыНужны idempotency key, срок хранения, cleanup и наблюдение переходовОстановить новые async-запуски и вернуть короткие операции на прямой путь
Общий workflow между сервисамиЕдиный статус для нескольких владельцев процессаПоявляется общий контракт, координатор и cross-service ownershipУдалить интеграцию нельзя одной заменой; сначала нужен план вывода потребителей
Ничего не менятьНе добавляет новый компонентСохраняет таймауты, повторы и неясную ответственностьОбратимость высокая, но исходный симптом остаётся
\n

В этой матрице нет итогового балла. Присвоить «3» за надёжность и «2» за стоимость можно только после определения шкалы, веса и источника данных. Без этого score создаёт видимость точности. Для небольшого решения достаточно назвать обязательное условие, показать trade-off и отметить, какие вопросы ещё не проверены.

\n
\"Синтетическая
Схема показывает форму сравнения, а не рейтинг реальных архитектур. Значения в иллюстрации синтетические: их нельзя трактовать как измерение production-нагрузки или стоимости.
\n

Запишите выбранный компромисс

\n

После сравнения решение должно отвечать на вопрос «что меняем и почему именно сейчас». Важно записать не только положительную сторону. Для status-модели положительный эффект — caller не удерживает один долгий запрос. Отрицательная сторона — сервис теперь владеет жизненным циклом операции, а значит, обязан определить повтор, истечение срока, очистку и поведение при падении worker.

\n
# ADR-0042: асинхронный экспорт с локальным status\n\n## Status\nAccepted\n\n## Context and Problem Statement\nЭкспорт может выйти за временную границу HTTP-запроса. Повтор\nзапроса должен быть безопасным, а запись операции — удаляемой.\n\n## Decision Drivers\n- подтверждение создания отдельно от завершения;\n- идемпотентный повтор;\n- явные expiry и owner состояния;\n- возможность ограниченного возврата.\n\n## Considered Options\n1. Синхронный ответ.\n2. Очередь и status у сервиса экспорта.\n3. Общий workflow между сервисами.\n\n## Decision Outcome\nВыбираем очередь и status у сервиса экспорта: граница владения\nостаётся локальной, а caller получает отдельный контракт состояния.\n\n## Consequences\nПлюс: долгую работу можно наблюдать и повторять безопасно.\nМинус: нужны хранение, expiry, cleanup, retry policy и метрики.\nНе делаем: не превращаем локальный status в общий workflow.\n\n## Confirmation\nПроверить контракт повторного запроса, переходы status, expiry,\nудаление результата и возврат короткой операции на direct path.
\n

Это воспроизводимый формат записи, но не готовая реализация очереди. Поля Accepted, expiry и owner не запускают процессы сами по себе. Их смысл должен быть закреплён в правилах репозитория: кто принимает запись, где она хранится, как связана с кодом и что считается подтверждением.

\n

Проверьте отрицательный путь, а не только happy path

\n

У решения есть границы «не делаем». Они защищают локальный механизм от незаметного расширения. В нашем примере сервис экспортов отвечает за собственные операции. Он не становится координатором платежей, уведомлений и нескольких downstream-сервисов. Если появился caller, которому нужен общий status процесса, это новое архитектурное условие, а не повод дописать исключение в старом ADR.

\n

Для каждой границы задайте обратный путь. Что остановит новые записи? Как найти незавершённые операции? Сколько времени хранить status? Что увидит клиент, если worker завершил файл, но не записал финальный статус? Ответы должны быть в контракте и runbook, а в ADR достаточно зафиксировать решение, цену и ссылки на эти проверки.

\n
От симптома к проверке и действию
СимптомГипотезаПроверкаДействие
Один idempotency key даёт два заданияКлюч проверяют после постановки в очередьПовторить запрос на тестовом хранилище и сравнить созданные idsПеренести дедупликацию к границе создания и добавить регрессионный тест
Status показывает done, а файл недоступенПереход статуса не связан с публикацией результатаПроверить порядок записи результата и смены status при сбоеСделать переход атомарным для контракта или описать промежуточное состояние
Старые операции растут без ограниченияExpiry записан в ADR, но нет владельца cleanupНайти job, журнал удаления и тест срока храненияНазначить owner и не считать retention выполненным без evidence
Новый сервис использует локальный statusГраница применимости не стала частью контрактаПроверить список consumers и требования к общему процессуОстановить расширение и создать successor ADR при новом scope
\n

Подтвердите связь записи с кодом

\n

Confirmation — это не фраза «всё проверено», а конкретный способ сравнить принятое решение с реализацией. Для учебного ADR достаточно проверить наличие ключевых разделов и затем отдельно запустить тесты контракта. Команда ниже не требует стороннего инструмента и возвращает ненулевой код, если в файле пропущен обязательный заголовок.

\n
set -eu\nadr='docs/decisions/ADR-0042-export-status.md'\ntest -s \"$adr\"\ngrep -q '^## Status$' \"$adr\"\ngrep -q '^## Context and Problem Statement$' \"$adr\"\ngrep -q '^## Decision Drivers$' \"$adr\"\ngrep -q '^## Considered Options$' \"$adr\"\ngrep -q '^## Decision Outcome$' \"$adr\"\ngrep -q '^## Consequences$' \"$adr\"\ngrep -q '^## Confirmation$' \"$adr\"\nprintf '%s\\n' 'ADR structure: OK'
\n

Запустите эту проверку из корня репозитория после создания файла и замените путь на свой. Она проверяет только структуру Markdown. Она не доказывает идемпотентность, безопасность, latency, корректность миграции или наличие owner. Для этого нужны публичные тесты, review кода, нагрузочная проверка, threat model или операционный dry run — в зависимости от решения.

\n

Сохраните историю и условие пересмотра

\n

Accepted означает, что команда приняла решение в указанном контексте. Это не обещание, что оно навсегда верно, и не сигнал для автоматического запуска работ. Если предпосылка изменилась, не переписывайте старый record так, будто он изначально описывал новый путь. Создайте новую запись, свяжите её с предыдущей и укажите, какой факт изменил выбор.

\n

Статусы тоже требуют локального соглашения. Proposed может означать «готово к обсуждению», Accepted — «принято», Rejected — «рассмотрено и отклонено», Superseded — «заменено новой записью». Названия встречаются в официальных шаблонах, но workflow approval, даты и права на изменение задаёт конкретная команда.

\n

Сигнал пересмотра лучше формулировать как наблюдаемое условие: появился consumer, требующий общего status; retention превысил согласованный предел; contract upstream изменился; worker не укладывается в заданный класс операций. Дата review может напомнить о проверке, но сама по себе не означает, что решение устарело. У сигнала должен быть владелец и действие, иначе это пожелание.

\n
## Reassessment\nОткрыть ADR-0043, если выполняется хотя бы одно условие:\n- второй сервис требует читать status как общий workflow;\n- изменился контракт повторного запроса;\n- retention больше согласованного срока;\n- cleanup не подтверждён тестом или операционным журналом.\nДействие: сравнить локальный status, общий workflow и прямой путь;\nстарую запись не редактировать.
\n

Рабочий порядок для команды

\n
  1. Сузьте вопрос. Назовите один значимый выбор, конкретную границу и затронутый контракт.
  2. Отделите факт от гипотезы. Запишите симптом и ссылку на источник; неподтверждённое вынесите в evidence gap.
  3. Назовите цену ошибки. Укажите, какие данные, время, права или обязанности будут потеряны при неверном пути.
  4. Соберите доступные варианты. Приведите их к одному уровню и добавьте «ничего не менять», если такой путь реален.
  5. Сравните по одинаковым drivers. Проверьте constraint fit, обратимость, операционную цену, владельца и остаточные риски.
  6. Запишите decision и отрицательный путь. Ясно укажите, что команда делает и чего намеренно не делает.
  7. Назначьте confirmation. Свяжите решение с тестом, контрактом, benchmark, threat model, runbook или другим проверяемым артефактом.
  8. Определите reassessment. Запишите наблюдаемый сигнал, владельца и действие; не полагайтесь на одну календарную дату.
  9. Сверьте ADR с реализацией. Если код выбрал другой вариант, исправьте расхождение или оформите новый decision, не переписывая историю.
\n

Ограничения применимости и критерий готовности

\n

ADR полезен для значимых решений: границ компонентов, API и data contracts, зависимостей, non-functional requirements, способов миграции и технологических направлений. Для тривиального локального рефакторинга отдельная запись может создать больше шума, чем контекста. Порог значимости определяется командой; важно сделать его явным.

\n

ADR не является design document, threat model, benchmark, тестовым раннером, runbook или approval-системой. Он не гарантирует, что команда нашла все альтернативы, и не превращает отсутствие данных в доказанную безопасность. Для персональных данных, security, legal и recovery нужны профильные проверки с отдельными владельцами. Не публикуйте в открытом ADR секреты, персональные данные и внутренние ссылки без разрешённого доступа.

\n

Запись можно считать готовой, если читатель без поиска по переписке отвечает на пять вопросов: какой симптом наблюдали; какое условие нельзя нарушить; почему отклонили альтернативы; какую цену принимает выбранный путь; какой сигнал откроет следующий ADR. Если не назван owner состояния или не существует проверка отрицательного пути, документ ещё фиксирует намерение, а не устойчивое решение.

\n

Проверяемые источники

\n" }