Files
progcode/editorial/agent-rewrites/120.json
T

2 lines
27 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"index":120,"slug":"editorial-2024-09-practice-adr-decisions","title":"ADR без бюрократии: как сохранить причину технического решения","excerpt":"Практический разбор ADR на учебном BFF-кейсе: как отделить факт от гипотезы, сравнить альтернативы, записать цену выбора и понять, когда нужен новый decision record.","contentHtml":"<p>После релиза в коде остаётся обходной путь: запрос идёт через отдельный слой, хотя прямой вызов короче. Через полгода обсуждение исчезает из чата, авторы переключаются на другие задачи, а reviewer видит только итоговый diff. Симптом легко узнать: команда снова спорит, зачем существует условие, очередь или дополнительная граница.</p><p>Цена ошибки выше стоимости потерянного контекста. Если удалить защиту слишком рано, вернётся старое ограничение. Если оставить её навсегда, команда продолжит платить за лишнее состояние, тесты и поддержку. По одному коду нельзя восстановить, какой риск когда-то перевешивал. Нужна короткая запись, связывающая наблюдаемую проблему, выбор и принятую цену.</p><p>Такую запись обычно называют ADR (Architecture Decision Record). В этой статье разберём практический маршрут для одного решения на границе BFF (Backend for Frontend): как понять, нужен ли кэш, какие варианты сравнить и как записать результат так, чтобы он не стал ложным разрешением на любую будущую оптимизацию.</p><h2>Что именно сохраняет ADR</h2><p>ADR фиксирует один значимый технический выбор и его rationale — объяснение, почему выбран этот путь. Минимальное содержимое: контекст и проблема, рассмотренные варианты, decision и последствия. Для каждого утверждения полезно оставить источник: контракт, тест, измерение, issue или ссылку на другой документ.</p><p>У записи есть границы ответственности. Ticket описывает работу и срок. Code review хранит обсуждение конкретного изменения. Test проверяет поведение. Runbook описывает операционное действие. ADR отвечает на другой вопрос: почему команда выбрала одну допустимую форму системы вместо других. Ссылка на ticket помогает найти детали, но не заменяет rationale.</p><p>Статус тоже не является переключателем. <em>Proposed</em> означает, что запись подготовлена к обсуждению. <em>Accepted</em> означает, что команда приняла решение. <em>Superseded</em> означает, что новый ADR заменил старый и объяснил изменение. Ни один статус сам по себе не запускает код, миграцию, approval или проверку отката.</p><h2>Учебный симптом на границе BFF</h2><p>Возьмём ограниченный пример. SSR-приложение обращается через BFF к каталогу предложений. Для одного экрана одинаковый справочник читается много раз за короткий интервал. Прямой запрос проще и всегда получает актуальный ответ, но увеличивает число обращений к upstream. Локальный кэш может сократить повторные чтения, однако добавляет TTL, риск устаревшего ответа и обязанность очищать состояние.</p><p>Это не отчёт о конкретной production-системе: в статье нет реальных latency, traffic, hit ratio или экономии. Сценарий нужен, чтобы воспроизвести форму анализа. В своём проекте подставьте контракт источника, допустимую свежесть, класс данных, owner и измерение. Если эти сведения неизвестны, ADR должен назвать их открытым вопросом, а не превращать предположение в факт.</p><p>Сначала запишем наблюдаемое и неизвестное отдельно:</p><pre><code>Наблюдаемый факт: один экран повторяет чтение одного справочника.\nИсточник факта: trace или счётчик запросов, приложенный к задаче.\nГипотеза: часть чтений можно обслужить из bounded local cache.\nОбязательное условие: ответ не старше согласованного TTL.\nНеизвестно: разрешает ли upstream такой уровень устаревания.\nЦена ошибки: устаревшие данные или рост нагрузки на upstream.</code></pre><p>Важна именно последовательность. «Добавим кэш, потому что медленно» — решение без доказанной причины. Сначала нужно подтвердить повторяемость чтений и спросить владельца контракта, допустима ли заданная свежесть. Если upstream требует read-after-write для этого справочника, локальный кэш не проходит constraint fit независимо от удобства реализации.</p><h2>Как сравнивать варианты на одной шкале</h2><p>Сравнивайте решения, а не рекламные формулировки. Для этого задайте одинаковые поля: соответствие обязательному условию, обратимость, evidence gap и стоимость эксплуатации. «Проще» без указания владельца и границы ничего не измеряет. Вариант «ничего не менять» тоже стоит назвать, если он действительно доступен: иногда дополнительное состояние опаснее лишнего запроса.</p><div class=\"table-scroll\"><table><caption>Учебная матрица для решения о кэше на BFF-границе</caption><thead><tr><th scope=\"col\">Вариант</th><th scope=\"col\">Что закрывает</th><th scope=\"col\">Цена и риск</th><th scope=\"col\">Что проверить</th></tr></thead><tbody><tr><td>Прямой запрос</td><td>Актуальность ответа без локального состояния</td><td>Повторная нагрузка на upstream; нет TTL и cleanup</td><td>Лимит запросов, latency и допустимость текущей нагрузки</td></tr><tr><td>Bounded local cache</td><td>Повторное чтение в пределах объявленного TTL</td><td>Устаревший ответ, invalidation, память и владелец кэша</td><td>Контракт freshness, класс данных, hit ratio и путь direct read</td></tr><tr><td>Общий cache-сервис</td><td>Разделяемое состояние для нескольких потребителей</td><td>Новый сервис, сеть, права, отказ и cross-team owner</td><td>Действительно ли нужен общий владелец и как переживается недоступность</td></tr><tr><td>Ничего не менять</td><td>Сохраняет простую модель и отсутствие нового состояния</td><td>Нагрузка остаётся; возможно, проблема не подтверждена</td><td>Повторить измерение и проверить, есть ли пользовательский эффект</td></tr></tbody></table></div><p>Матрица не выдаёт победителя автоматически. Она заставляет привести варианты к сопоставимому масштабу. Если для кэша известен TTL, а для общего сервиса написано только «сложно», данные ещё не симметричны. Если используете баллы и веса, приложите шкалу и объясните, какие риски модель не учитывает. Итоговый score помогает провести разговор, но не доказывает корректность решения.</p><figure><img src=\"/assets/editorial/2024/adr-decisions-2024-decision-flow.svg\" alt=\"Поток ADR от наблюдаемого симптома и цены ошибки через ограничения и альтернативы к принятому решению, проверке реализации и successor ADR при изменении условий.\" loading=\"lazy\" /><figcaption>ADR связывает контекст и выбор, но реализация и evidence проходят отдельную проверку. Ветвь successor ADR сохраняет историю, если исходное условие изменилось.</figcaption></figure><h2>Минимальная запись решения</h2><p>После проверки контракта допустим учебный исход: upstream разрешает ограниченную свежесть, справочник не содержит данных, требующих немедленного read-after-write, а direct read остаётся доступным при промахе или отключении кэша. Тогда можно предложить локальное bounded state. Ниже — запись в формате, который можно положить в каталог decisions. Она не создаёт кэш и не утверждает измеренный результат.</p><pre><code># ADR-0042: bounded cache at the BFF boundary\n\n## Status\nProposed\n\n## Context and problem\nSSR повторяет чтение одного справочника. Upstream разрешает\nответ не старше согласованного TTL. Для критичных изменений\nтребуется direct read.\n\n## Options considered\n- direct read;\n- bounded local cache with explicit TTL;\n- shared cache service.\n\n## Decision\nНачать с bounded local cache только для этого read path.\nПри недоступном кэше читать upstream напрямую.\n\n## Consequences\nПлюс: повторные чтения ограничены TTL.\nЦена: owner состояния, expiry, наблюдение за freshness и cleanup.\n\n## Not in scope\nНе вводим shared cache-сервис и не распространяем решение\nна другие callers без нового ADR.\n\n## Reassessment\nОткрыть successor ADR при изменении контракта freshness,\nпоявлении чувствительных данных или нового потребителя.</code></pre><p>Учебный фрагмент показывает причинность: симптом связан с ограничением, варианты перечислены, цена названа, а отрицательный путь ограничивает scope. В настоящую запись добавьте ссылки на подтверждённый контракт, тест, trace или controlled experiment. Статус <em>Proposed</em> означает, что документ ещё нужно проверить владельцу upstream и участникам, которых затрагивает кэш.</p><h2>Как связать ADR с реализацией и evidence</h2><p>Решение и доказательство нельзя слить в один абзац. После принятия ADR команда отдельно проверяет реализацию: TTL действительно ограничен, ключ кэша не смешивает разные контексты, промах или ошибка не блокируют direct read, а чувствительные данные не попадают в неподходящее хранилище. Это уже область тестов, threat model, ревью и наблюдаемости.</p><p>Полезно сформулировать evidence question до изменения кода. Например: «Для выбранного read path доля повторных запросов превышает порог X, а upstream разрешает TTL Y секунд». Числа X и Y должны прийти из вашего контракта и измерения; в учебной статье их нельзя выдумывать. По итогам controlled test в ADR можно добавить ссылку на артефакт и уточнить последствия. Сам факт наличия ссылки не превращает эксперимент в гарантию для всех нагрузок.</p><p>Связь с кодом должна быть двусторонней и короткой: ADR ссылается на реализацию, тест и метрику; code review ссылается на ADR, чтобы reviewer мог проверить границу решения. Если diff добавляет shared invalidation, новую схему данных или другой срок хранения, это уже не «деталь», а возможное новое решение. Не прячьте его в implementation note.</p><h2>Когда принятый ADR нельзя редактировать</h2><p>Предпосылки решения со временем меняются. Upstream может начать требовать строгую свежесть, появится второй потребитель, изменится класс данных или стоимость shared service станет приемлемой. В такой ситуации старый ADR не следует переписывать задним числом. Иначе исчезнет ответ на вопрос, почему прежний код был разумным при старых условиях.</p><p>Создайте новый record со статусом <em>Proposed</em>, свяжите его с исходным и опишите, что именно изменилось. После принятия нового решения старое получает <em>Superseded</em>. Это не формальность: append-only история отделяет прежний компромисс от нового и помогает безопасно читать старые ссылки в issue, review и runbook.</p><p>Дата пересмотра полезна только вместе с действием и владельцем. Дата сама не откроет документ. Надёжнее написать условие: «пересмотреть, если контракт freshness изменился или список callers вышел за пределы BFF». Для измеримых условий добавьте источник и того, кто должен инициировать review.</p><h2>Симптом → причина → проверка → действие</h2><div class=\"table-scroll\"><table><caption>Типовые ошибки при создании ADR</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Следующее действие</th></tr></thead><tbody><tr><td>Записан только победитель</td><td>Решение приняли по вкусу, а не по constraint</td><td>Заполнить одинаковые поля для двух-трёх вариантов</td><td>Добавить downside выбранного пути и причины отказа</td></tr><tr><td>В Consequences обещана экономия</td><td>Учебную гипотезу выдали за измерение</td><td>Найти trace, benchmark или метрику с единицей измерения</td><td>Убрать число либо добавить ссылку на evidence</td></tr><tr><td>Кэш расширяется на новых callers</td><td>Граница применения осталась неявной</td><td>Сверить список потребителей и contract freshness</td><td>Остановить расширение и открыть новый ADR</td></tr><tr><td>Флаг выключили, но старый путь удалили</td><td>Rollback смешали с cleanup</td><td>Проверить, может ли система вернуться к direct read</td><td>Разделить изменение поведения и удаление ветки</td></tr><tr><td>Новый ADR переписывает старый</td><td>Историю принимают за текущую конфигурацию</td><td>Проверить ссылки и статусы двух записей</td><td>Оставить старый текст, связать successor и объяснить drift</td></tr></tbody></table></div><p>Эта таблица применима как диагностическая последовательность, а не как чек-лист полноты архитектуры. Например, ошибка кэша может быть не в выборе варианта, а в неверном ключе или cache-control заголовке. ADR помогает увидеть исходный компромисс, но не заменяет проверку конкретной реализации.</p><h2>Практический порядок работы</h2><ol><li><strong>Сузьте вопрос.</strong> Назовите одного caller, одну границу и одно обязательное условие. «Ускорить систему» слишком широко; «ограничить повторные чтения этого справочника при допустимой свежести» проверяемо.</li><li><strong>Соберите факты.</strong> Укажите наблюдаемый симптом, источник, дату наблюдения и цену неверного выбора. Отделите гипотезу от того, что действительно видно в trace, контракте или тесте.</li><li><strong>Проверьте constraint.</strong> Спросите владельца upstream о freshness, лимитах, правах и классе данных. Неподтверждённое условие оставьте evidence gap.</li><li><strong>Перечислите варианты.</strong> Добавьте два-три пути одного масштаба. Включите «ничего не менять», если он не нарушает обязательное условие.</li><li><strong>Сравните одинаково.</strong> Для каждого варианта назовите границу, owner, обратимость, операционную цену, неизвестное и отрицательный путь.</li><li><strong>Запишите decision.</strong> Формулировка должна сказать, что делаем и чего не делаем. Не объявляйте учебный пример результатом production.</li><li><strong>Проведите review.</strong> Участники должны проверить context, alternatives, consequences, status и затронутые контракты до начала реализации.</li><li><strong>Свяжите evidence.</strong> После controlled test или релиза добавьте ссылки на код, тест, trace, метрику или runbook. Ссылка не заменяет саму проверку.</li><li><strong>Проверьте drift.</strong> При новом consumer, изменении контракта или другой цене решения откройте successor ADR, а не дописывайте старый задним числом.</li></ol><h2>Ограничения применимости</h2><p>ADR не заменяет design document, threat model, migration plan, benchmark, test plan, runbook или incident review. Он не измеряет latency, надёжность, стоимость и безопасность без соответствующего evidence. Он также не гарантирует, что команда нашла все варианты: качество зависит от состава участников, доступных фактов и времени на review.</p><p>Не каждую мелкую правку стоит оформлять отдельной архитектурной записью. Кандидатами являются решения, которые меняют структуру, API или другой опубликованный контракт, качество сервиса, зависимости, владение состоянием либо плохо обратимы. Граница «значимости» командная; её лучше закрепить в локальном шаблоне, чтобы не превращать ADR в журнал каждого переименования.</p><p>Локальный кэш из примера нельзя переносить на персональные или чувствительные данные без отдельного анализа хранения, доступа и очистки. BFF не становится владельцем бизнес-истины только потому, что временно хранит ответ. При строгой консистентности direct read или иной контракт может быть единственно допустимым вариантом.</p><p>Форматы ADR различаются. Можно хранить записи в Git, wiki или специальном каталоге, если команда сохраняет версионирование, доступность, единый шаблон и историю статусов. Конкретные поля вроде confidence, stakeholders и review trigger выбираются по риску решения. Источник формата не является разрешением игнорировать требования безопасности, права доступа и отраслевые процессы.</p><h2>Критерий готовности</h2><p>Новый читатель должен без поиска по чату ответить на пять вопросов: какой симптом наблюдали; какое ограничение обязательно; какие варианты сравнивали; какую цену принимает выбранный путь; что заставит открыть новый ADR. Он должен отличать подтверждённый факт от гипотезы и понимать, где проверяется реализация.</p><p>Финальная проверка короткая. Уберите из записи заголовок <em>Decision</em> и попросите коллегу восстановить выбор по Context, Options и Consequences. Если коллега не может назвать owner, отрицательный путь или сигнал пересмотра, ADR ещё не готов. Если запись обещает измеренный эффект без источника, эффект нужно убрать или измерить отдельно.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/adr-process.html\" target=\"_blank\" rel=\"noopener noreferrer\">AWS Prescriptive Guidance: Architectural decision record process</a> — описание контекста, decision, consequences, статусов, owner и процесса review; AWS отдельно объясняет, что принятый ADR не переписывают, а новое решение оформляют как successor.</li><li><a href=\"https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record\" target=\"_blank\" rel=\"noopener noreferrer\">Microsoft Learn: Maintain an architecture decision record (ADR)</a> — рекомендации по options, trade-offs, status, append-only истории и границе между ADR и подробным design guide.</li><li><a href=\"https://adr.github.io/madr/\" target=\"_blank\" rel=\"noopener noreferrer\">MADR: Markdown Architectural Decision Records</a> — официальный сайт проекта с lean-шаблонами, rationale, примерами и практикой версионирования решений.</li></ul>"}