2 lines
27 KiB
JSON
2 lines
27 KiB
JSON
{"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>"}
|