{ "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 определяет architectural decision как обоснованный выбор, значимый для архитектуры, а ADR — как запись одного такого выбора и его rationale, то есть причины. Коллекция записей образует decision log. Поэтому один ADR отвечает на один связный вопрос, а не пытается стать полной технической документацией системы.
\nВ шаблоне Michael Nygard минимальная форма состоит из Status, Context, Decision и Consequences. В MADR форма расширяется полями decision drivers, considered options, outcome и confirmation. Это полезные шаблоны, а не универсальный закон: команда может добавить владельца, ссылки на контракт, confidence или условие пересмотра.
\n| Артефакт | Главный вопрос | Что должно остаться внутри | Чего он не доказывает |
|---|---|---|---|
| ADR | Почему выбран этот вариант? | Контекст, ограничения, альтернативы, решение и последствия | Что код уже корректен во всех средах |
| Design document | Как устроить решение целиком? | Компоненты, потоки, контракты и детали реализации | Что выбранный дизайн принят как долгосрочное решение |
| Test | Какое поведение воспроизводится? | Условия, входы, ожидаемый результат, регрессия | Почему выбрана именно эта архитектура |
| Runbook | Что делать при операционном событии? | Команды, роли, stop condition и восстановление | Что исходный компромисс всё ещё применим |
Ссылка на ticket или commit полезна для навигации, но не заменяет rationale. И наоборот, ADR не должен превращаться в копию implementation guide. Если решение требует детальной схемы, threat model или плана миграции, запишите выбор в ADR и свяжите его с отдельным документом.
\nНачинайте с наблюдаемого симптома, а не с названия технологии. Например: «клиент вызывает экспорт, но операция иногда не укладывается в границу HTTP-запроса; при повторе непонятно, создан ли второй экспорт». Это факт и вопрос. Гипотеза «нам нужна очередь» появляется только после проверки длительности операции, повторяемости запроса и требований caller.
\nЗатем назовите цену ошибки. В этом сценарии неверный выбор может дать дубли экспорта, зависшие записи, неограниченное хранение результатов или расхождение между статусом и фактическим завершением. Цена должна быть привязана к системе: какой ресурс останется, кто его удаляет, какая граница времени действует, какой контракт увидит клиент.
\nПолезно записать decision drivers — факторы, по которым варианты будут сравниваться. Для экспорта это могут быть ограничение времени ответа, идемпотентность повторного запроса, наблюдаемость состояния, стоимость хранения и возможность вернуться к синхронному пути для коротких файлов. Если driver нельзя проверить или ему нет владельца, это пока предположение, а не доказательство.
\nПроблема: export иногда дольше жизни HTTP-запроса.\nОбязательные условия:\n- клиент получает подтверждение создания операции;\n- повтор запроса не создаёт второй экспорт;\n- состояние имеет срок хранения и владельца очистки.\nЦена ошибки: дубль работы или запись, которую никто не удаляет.\nВопрос решения: где хранить status и кто отвечает за его переходы?\nФрагмент — учебная заготовка. Он не сообщает реальную длительность, нагрузку или процент ошибок. В рабочей записи эти утверждения должны ссылаться на контракт, trace, тест, incident или другой доступный источник. Если данных нет, оставьте evidence gap и действие по его закрытию.
\nСравнение ломается, когда выбранный вариант описан конкретно, а остальные получают ярлык «сложно». Приведите варианты к одному уровню и задайте одинаковые вопросы: выполняет ли вариант обязательные условия, где хранится состояние, как работает повтор, кто поддерживает очистку, как выглядит возврат и какое evidence уже есть.
\n| Вариант | Что закрывает | Цена и риск | Путь назад |
|---|---|---|---|
| Синхронный ответ | Простая модель для короткой операции | Не выдерживает временную границу; повтор может повторить работу | Не требуется отдельное состояние, но граница остаётся нерешённой |
| Очередь и status у сервиса экспорта | Отделяет подтверждение создания от завершения работы | Нужны idempotency key, срок хранения, cleanup и наблюдение переходов | Остановить новые async-запуски и вернуть короткие операции на прямой путь |
| Общий workflow между сервисами | Единый статус для нескольких владельцев процесса | Появляется общий контракт, координатор и cross-service ownership | Удалить интеграцию нельзя одной заменой; сначала нужен план вывода потребителей |
| Ничего не менять | Не добавляет новый компонент | Сохраняет таймауты, повторы и неясную ответственность | Обратимость высокая, но исходный симптом остаётся |
В этой матрице нет итогового балла. Присвоить «3» за надёжность и «2» за стоимость можно только после определения шкалы, веса и источника данных. Без этого score создаёт видимость точности. Для небольшого решения достаточно назвать обязательное условие, показать trade-off и отметить, какие вопросы ещё не проверены.
\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У решения есть границы «не делаем». Они защищают локальный механизм от незаметного расширения. В нашем примере сервис экспортов отвечает за собственные операции. Он не становится координатором платежей, уведомлений и нескольких 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 |
Confirmation — это не фраза «всё проверено», а конкретный способ сравнить принятое решение с реализацией. Для учебного ADR достаточно проверить наличие ключевых разделов и затем отдельно запустить тесты контракта. Команда ниже не требует стороннего инструмента и возвращает ненулевой код, если в файле пропущен обязательный заголовок.
\nset -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 — в зависимости от решения.
\nAccepted означает, что команда приняла решение в указанном контексте. Это не обещание, что оно навсегда верно, и не сигнал для автоматического запуска работ. Если предпосылка изменилась, не переписывайте старый 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старую запись не редактировать.\nADR полезен для значимых решений: границ компонентов, API и data contracts, зависимостей, non-functional requirements, способов миграции и технологических направлений. Для тривиального локального рефакторинга отдельная запись может создать больше шума, чем контекста. Порог значимости определяется командой; важно сделать его явным.
\nADR не является design document, threat model, benchmark, тестовым раннером, runbook или approval-системой. Он не гарантирует, что команда нашла все альтернативы, и не превращает отсутствие данных в доказанную безопасность. Для персональных данных, security, legal и recovery нужны профильные проверки с отдельными владельцами. Не публикуйте в открытом ADR секреты, персональные данные и внутренние ссылки без разрешённого доступа.
\nЗапись можно считать готовой, если читатель без поиска по переписке отвечает на пять вопросов: какой симптом наблюдали; какое условие нельзя нарушить; почему отклонили альтернативы; какую цену принимает выбранный путь; какой сигнал откроет следующий ADR. Если не назван owner состояния или не существует проверка отрицательного пути, документ ещё фиксирует намерение, а не устойчивое решение.
\n