ADR: запись архитектурного решения, которая переживёт автора
Инженер открывает payments/outbox.go и видит странное: события пишутся в таблицу Postgres, а отдельный
воркер вычитывает её и публикует в Kafka; прямая публикация была бы на двадцать строк короче. Он идёт
по следам: git blame даёт коммит «fix flaky publishing», коммит ведёт в PR, в PR написано «см. тред
в Slack», тред удалён политикой хранения за 90 дней, автор уволился год назад. Вывод единственно
возможный: «наверное, историческое». Через три недели он «упрощает» — и через месяц платежи теряют
события при откате транзакции. Ровно тот баг, из-за которого outbox и появился.
Цена отсутствующего документа тут — не «не задокументировали», а повторно воспроизведённый инцидент и месяц работы. Документ, который его предотвращал, занимал страницу и писался полчаса. Он называется ADR — Architecture Decision Record.
ADR пишется не для того, кто принимает решение, а для того, кто через годы решит, можно ли это решение снять. Его читатель — археолог, а решение читателя — «трогать или не трогать».
Это следствие принципа из главы «Читатель и решение»: адресат и решение определяют жанр. У ADR они необычные — адресата ещё нет в компании, а решение он примет без вас. Отсюда все особенности формата, включая самую странную: ADR не редактируют. Здесь речь о том, как решение записать; про то, как его принимать (критерий значимости, trade-offs, ATAM, fitness functions), — глава «Архитектурные решения».
Что такое ADR и с чем его путают
Формат придумал Майкл Найгард в 2011 году
(«Documenting Architecture Decisions»),
и держится он на одном свойстве: маленький. Одно решение — один пронумерованный файл в репозитории
рядом с кодом. Остальное — следствия: файл с пятью решениями не отменить по частям; номер ADR-0021
остаётся адресом навсегда, даже после отмены; запись описывает прошлое, а не настоящее.
| Жанр | Когда пишется | Читатель | Решение читателя | Обновляется? |
|---|---|---|---|---|
| RFC / дизайн-док | до решения | коллеги, техлид | согласиться с подходом | правками, потом замирает |
| ADR | в момент решения | инженер через 1–3 года | менять ли то, что стоит | нет, только статус |
| README | всегда | новый человек, интегратор | как начать работать | постоянно |
| Постмортем | после сбоя | команда и смежники | что поменять в системе | нет, append-only |
Частая путаница — звать RFC «ADR» (двадцать страниц обсуждения там, где нужна страница вывода) или писать ADR вместо документации системы. ADR и RFC — пара: RFC ведёт к решению, ADR его фиксирует.
Читатель ADR — археолог, а не участник
Вспомните, когда вы последний раз открывали ADR: почти наверняка не «читая документацию», а собираясь что-то поменять и наткнувшись на непонятное.
Центр тяжести справа: первые недели документ почти не нужен, все в контексте, а пик полезности ADR наступает после того, как автор про него забыл. Отсюда читатели и их решения:
- Инженер, собравшийся менять код: «это защита от чего-то реального или карго-культ?» Нужны последствия и цена отмены.
- Новый техлид: «где осознанный компромисс, а где случайность?» Нужны движущие силы и отвергнутые варианты; первые недели такого чтения — «Первые 90 дней».
- Смежная команда перед похожим выбором: нужен контекст с цифрами, чтобы сверить со своими.
- Вы сами через год («я вообще помню, почему так?» — не помните, проверено) и аудитор, которому нужен сам след принятия решения: законный, но другой читатель.
Тест на готовность: дайте запись тому, кто не участвовал в обсуждении, и спросите, что сломается, если решение отменить. Не ответил — ADR не написан.
Почему запись о прошлом не устаревает
Главная болезнь документации — устаревание; ей посвящена глава «Почему документация устаревает». Но у ADR есть структурное свойство, которое надо понять здесь. Сравните:
<!-- Плохо, описание настоящего --> Сервис публикует события напрямую в Kafka из обработчика.
<!-- Хорошо, запись о прошлом --> В марте 2024 года мы решили публиковать события через
транзакционный outbox, потому что прямая публикация теряла события при откате транзакции БД.
Первое станет ложью в день, когда кто-то поменяет код, и никто не заметит момента. Второе не может стать ложью никогда: даже если сегодня всё переписано, в марте 2024 года мы действительно так решили и по такой причине. Это и есть структурный ответ на устаревание, а не призыв «обновляйте документацию»: просрочить документ в прошедшем времени с датой невозможно. Устаревают тексты, претендующие на «текущее состояние», — README, схемы, справочники, — и лечится это сменой жанра там, где смена возможна, а не дисциплиной.
После статуса
acceptedтело записи не редактируется. Изменились обстоятельства — пишется новая запись, отменяющая старую. Старая остаётся со статусомsuperseded by ADR-0034.
Правило кажется бюрократическим, пока не увидишь альтернативу: отредактированный ADR через год не соответствует ни прошлому, ни настоящему — контекст от 2024-го, решение от 2025-го, последствия от кого-то третьего, — и это хуже отсутствия документа, потому что выглядит достоверно. Исключения ровно три: опечатки, битые ссылки и строка статуса.
Неизменяемость решает половину проблемы; вторую решают механизмы, а не добрая воля. Близость
к коду: docs/adr/0021-outbox.md в том же репозитории — расстояние между решением и кодом главный
предиктор того, прочтут ли запись. Один PR на решение и запись: никаких «допишу потом».
Генерируемый в CI индекс: оглавление руками протухает на четвёртой записи. Владелец и критерий
пересмотра: команда через CODEOWNERS плюс сигнал для пересмотра.
Порог тоже структурный вопрос. ADR нужен, если отмена решения дороже спринта, если оно задевает контракт наружу или интерфейс между командами, либо если через год кто-то захочет «упростить» и сломает. Всё обратимое живёт в описании коммита («Совместная работа» в треке git): требуя ADR на каждый выбор библиотеки, организация получает не журнал, а шум, в котором тонут важные записи.
Анатомия: какое поле какой вопрос закрывает
Полей немного, каждое отвечает ровно на один вопрос читателя; поле без вопроса в шаблоне не нужно.
Заголовок
Единственное, что прочтут все: он попадёт в индекс, в поиск, в ссылку из кода. Заголовок содержит решение, а не тему.
<!-- Плохо --> ADR-0021: Kafka
<!-- Плохо --> ADR-0021: Обсуждение подхода к доставке сообщений
<!-- Хорошо --> ADR-0021: Публикуем доменные события через outbox, а не напрямую в Kafka
<!-- English --> ADR-0021: Publish domain events via transactional outbox instead of direct Kafka writes
«Kafka» — тема: непонятно, приняли её, отвергли или настраивали. «Обсуждение подхода» описывает
процесс, а не результат, и выдаёт RFC, переодетый в ADR. Переписанный вариант несёт что делаем, для
чего и от чего отказались; последнее — половина ценности: предложивший через год «а давайте
напрямую» увидит это прямо в индексе. Английский шаблон <действие> via <механизм> instead of <альтернатива> берите как есть.
Статус
Одна строка и единственное изменяемое место записи.
Про rejected отдельно: организации выбрасывают отклонённые записи — и каждый год заново обсуждают
«а почему мы не на GraphQL?». Запись ADR-0018: Отказались от перехода на GraphQL (rejected) экономит
недели: это документ под решение «не возвращаться к теме», и удалять такие записи нельзя.
Контекст
Самое сложное поле и то, ради которого ADR читают: мир на момент решения — цифры, ограничения, сроки. Прошедшее время, даты, числа. Плохая версия — та, которую пишут в 80 % случаев:
Существующая архитектура публикации событий имеет ряд недостатков и не полностью соответствует
требованиям надёжности. В связи с ростом нагрузки и повышением требований бизнеса было принято
решение пересмотреть подход. Также следует учитывать, что команда обладает ограниченными ресурсами.
Разбор по болезням, каждая встречается и отдельно. «Ряд недостатков» — через два года не проверить, сохранились ли они: их нет в тексте. «Требованиям надёжности» — каким? Ни числа, ни ссылки на SLO. «В связи с ростом нагрузки» — с какой на какую? Если нагрузка упала вдвое, решение, возможно, пора отменять, но узнать это неоткуда. «Было принято решение» — пассив без субъекта: кем принято, с кем спорить? «Ограниченные ресурсы» — они ограничены всегда.
На 2024-03-14 сервис `payments` публикует события `PaymentCaptured` прямо из обработчика
HTTP-запроса, после коммита транзакции в Postgres.
За февраль — 3 инцидента (INC-2291, INC-2307, INC-2318): при откате транзакции после успешной
публикации потребители получали события о платежах, которых не было. Разбор каждого занимал
2–4 часа, компенсации проводились вручную.
Ограничения на момент решения:
- Kafka — управляемый кластер, брокер 2.6, без транзакций; апгрейд ориентировочно в 2025 году.
- Postgres общий с `orders`, новые таблицы согласуются с командой Orders.
- SLO публикации: 99.9 % событий за 60 с. Команда: 3 инженера, до релиза биллинга 6 недель.
Изменилась не красота, а проверяемость. Через два года читатель открывает каждую строку
и спрашивает: «это ещё так?» Kafka обновили до версии с транзакциями — основание отпало; orders
разъехались в свою базу — ограничения нет. Контекст с числами превращается в чек-лист для будущего
пересмотра, из прилагательных — в шум (здесь же работает вся глава
«Ясность»).
Движущие силы
Список того, что вы оптимизировали, в порядке приоритета: он объясняет, почему из двух разумных вариантов выбран менее очевидный.
<!-- Плохо -->
Мы стремились выбрать оптимальное решение, обеспечивающее надёжность, производительность
и удобство поддержки.
<!-- Хорошо -->
1. Ноль событий о несуществующих платежах — важнее, чем задержка доставки.
2. Уложиться в 6 недель до релиза биллинга — важнее, чем «правильная» схема на будущее.
3. Не требовать изменений в коде команды Orders.
Сознательно НЕ оптимизировали: задержку доставки (готовы к +2–5 с) и красоту схемы БД.
Первый вариант перечисляет всё хорошее сразу — а решение всегда есть выбор между хорошими вещами, и список без приоритета не помогает ни выбрать, ни понять выбор задним числом. Во втором есть то, чего почти никогда нет в ADR: явно названное, чем пожертвовали, — строка, которая отвечает на будущее «почему тут лишние две секунды» и предотвращает «оптимизацию».
Рассмотренные варианты
Три-пять вариантов, у каждого — конкретная причина отказа, а не «не подошёл».
**A. Прямая публикация с ретраями в памяти** (текущее состояние).
Отклонён: ретраи не переживают рестарт пода, а рестарты дали 2 инцидента из 3.
**B. Транзакции Kafka (exactly-once).**
Отклонён: нужен брокер ≥ 2.7, апгрейд управляемого кластера вне нашего контроля.
Вернуться после апгрейда — см. критерий пересмотра.
**C. CDC через Debezium.**
Отклонён: даёт то же свойство, но добавляет Kafka Connect в зону ответственности команды
из 3 человек. Оценка 3 недели против 4 дней для outbox — не проходит по сроку.
**D. Транзакционный outbox с воркером-публикатором.** Выбран.
Блок защищает от повторного анализа (предложивший Debezium увидит, что вариант взвешивали), честно фиксирует, что отказ был не по существу, а по срокам и людям, и превращает вариант B в отложенное решение с условием возврата. Типичная деградация — «варианты для галочки»: соломенные чучела с отказом вида «избыточно сложно». Не было анализа — честнее написать «других вариантов не рассматривали из-за срока».
Решение
Одно-два предложения, активный залог, первое лицо множественного числа — плюс где это в коде.
<!-- Bad -->
It was decided that the system would leverage an outbox-based approach in order to ensure
that events are eventually delivered in a reliable manner.
<!-- Good -->
We write domain events to the `payments.outbox` table inside the same transaction that changes
payment state. A worker (`cmd/outbox-publisher`) polls the table every 500 ms and publishes to
Kafka with at-least-once semantics; consumers deduplicate by `event_id`.
Разбор английской пары. It was decided that — пассив без субъекта: неизвестно, кто решил и с кем
спорить. would leverage — модальность и мода вместо действия, use короче и точнее.
in order to ensure that ... in a reliable manner — четырнадцать слов, заменённых на механизм
(at-least-once, дедупликация по event_id) и имя модуля. По-русски так же: «мы пишем», а не «осуществляется запись».
Последствия
Раздел, отличающий ADR от протокола совещания: что стало лучше, что хуже и что теперь нельзя. Все три категории обязательны: одни плюсы означают, что trade-off не осознан.
<!-- Плохо -->
Данное решение позволит повысить надёжность доставки событий и улучшить сопровождаемость.
<!-- Хорошо -->
Положительные:
- События не теряются при откате транзакции и переживают рестарт пода: запись события и изменение
платежа атомарны, неотправленное остаётся в таблице.
Отрицательные (цена, которую мы платим):
- Задержка доставки выросла с ~50 мс до 0.5–2.5 с (интервал опроса плюс батч).
- Таблица `payments.outbox` растёт на ~1.2 ГБ в месяц: нужен партишенинг и уборка старше 7 дней.
- Новый компонент с дежурством и алертом на возраст старейшей неопубликованной строки.
Что теперь нельзя:
- Публиковать в Kafka напрямую из кода `payments`: линтер запрещает импорт kafka-клиента вне
`cmd/outbox-publisher` (`.golangci.yml`, правило `depguard`).
- Полагаться на порядок событий между агрегатами: он гарантирован внутри `aggregate_id`.
«Позволит повысить надёжность» — обещание без проверяемого критерия. Переписанная версия называет
цену в числах, фиксирует эксплуатационные обязательства, перечисляет запреты и — важнее всего —
привязывает запрет к исполняемой проверке: строка про depguard означает, что нарушить решение
труднее, чем не знать о нём. Раздел «что теперь нельзя» недооценён: решения чаще ломают не
из несогласия, а по незнанию.
Критерий пересмотра
Одна-две строки: когда запись открывать заново. Без них журнал превращается в кладбище, где не понять, какие записи ещё живы.
Вернуться к решению, если наступит любое из:
- кластер Kafka обновлён до ≥ 2.7 с включёнными транзакциями (тогда вариант B дешевле);
- задержка доставки попадает в SLO продукта (сейчас её там нет);
- размер outbox превышает 20 ГБ или лаг публикации регулярно больше 60 с.
Владелец: команда Payments. Пересмотр по сигналу, не по календарю.
Последняя строка принципиальна: «пересмотреть через год» проигнорируют, а сигнальный критерий срабатывает по факту изменения и вешается на алерт или чек-лист апгрейда.
Журнал как связный граф, а не папка файлов
Ценность появляется, когда между записями есть связи: что кого отменяет, что кого уточняет, что к какому модулю относится.
Прямая публикация
superseded"] A21["ADR-0021
Транзакционный outbox
superseded"] A26["ADR-0026
Дедупликация по event_id
accepted"] A34["ADR-0034
Транзакции Kafka
accepted"] end A7 -->|"отменена"| A21 -->|"отменена"| A34 A21 -->|"уточняется"| A26 -.->|"остаётся в силе"| A34 M1 -.->|"// см. ADR-0034"| A34
Цепочка отмен читается как история: открыв ADR-0034, инженер попадает в ADR-0021, оттуда
в ADR-0007 и видит три поколения решения с причинами переходов — из редактируемого документа этого
не получить. ADR-0026 пережил отмену родителя (дедупликация нужна и при транзакциях Kafka): связи
образуют граф, а не список, и в новой записи пишут, какие следствия старой сохраняются. Ссылки
двусторонние — односторонняя рвётся при переименовании. Связность проверяется в CI: скрипт следит,
что каждый superseded by указывает на существующий файл (пример линтера — в
главе про архитектурные решения,
про пайплайн — в «Тесты в CI»).
Запись едет вместе с кодом
Момент написания влияет на качество ADR сильнее, чем шаблон. Правильный момент — тот же PR, что и изменение.
Первый коммит ветки — черновик со статусом proposed: написать контекст до кода это ещё и способ
проверить, понимаешь ли задачу, и половина неудачных подходов отмирает здесь. Статус меняется
на accepted перед мержем, в той же ветке, — ревьюер видит решение и запись одним диффом и спорит
с формулировкой как с кодом («Ревью текста»). Отмена тоже коммит.
Обратный порядок («сделаем, а ADR напишем потом») даёт предсказуемый результат: записи не появляется
вовсе, а если появляется — это реконструкция по памяти.
Инструменты почти не влияют на результат — влияет близость к коду и требование ревью. Из рабочих: adr-tools (CLI поверх соглашения об именах), MADR (шаблон), log4brains.
Y-statement: ADR в одну строку
Когда решение маленькое, но не тривиальное, полный шаблон отпугивает — и запись не появляется. Для
этого есть сжатая форма Олафа Циммермана:
«в контексте <ситуация>, столкнувшись с <проблема>, мы выбрали <вариант>, отвергнув
<альтернативы>, чтобы достичь <качество>, приняв как цену <последствия>».
В контексте публикации событий из payments, столкнувшись с потерей согласованности при откате
транзакции, мы выбрали транзакционный outbox, отвергнув транзакции Kafka (недоступны на брокере 2.6)
и Debezium (не укладываемся в срок), чтобы гарантировать отсутствие событий о несуществующих
платежах, приняв как цену задержку 0.5–2.5 с и новый компонент.
Одна фраза содержит контекст, проблему, решение, альтернативы, цель и цену — минимально допустимый ADR.
Когда ADR пишут ради процесса
Честная часть главы: заметная доля ADR в больших организациях пишется не для читателя, а для процесса. Как распознать это у себя:
| Симптом | Что за ним стоит |
|---|---|
| Пятнадцать записей с одинаковой датой про решения двухлетней давности | Журнал реконструирован задним числом перед аудитом |
| «Рассмотренные варианты» есть везде, альтернативы описаны в одну строку | Шаблон обязателен, анализа не было: поля заполнены под чек-лист |
Все записи accepted, ни одной rejected или superseded |
Реальные отмены происходят мимо журнала |
| Пишет не тот, кто принимал решение, а «ответственный за документацию» | Запись отделена от работы, контекст восстановлен по слухам |
| Шаблон на 14 разделов, из которых 9 всегда «N/A»; никто не вспомнит, когда ссылался на ADR в споре | Формат сделан под аудит: журнал пишут, но не читают |
Два теста дают ответ за десять минут. Тест на ссылку: поищите номера ADR в PR, тикетах и коде за полгода — ноль упоминаний означает мёртвый журнал независимо от его объёма. Тест на конфликт: последний архитектурный спор закончился записью в журнале или сообщением в чате? Не участвует в спорах — не участвует и в решениях.
Что делать, по убыванию реалистичности: понизить порог (вместо тяжёлого шаблона — Y-statement плюс «Последствия», и записей станет больше, а не меньше); сузить область значимых решений; перенести в PR, раз запись всё равно рождается вместе с кодом; признать законного читателя — если ADR нужны аудиту (SOC 2, ISO, регуляторные требования — см. «Enterprise-разработка»), адресат есть, просто это не инженер: ему нужен след решения, а не глубина анализа, и плохо не то, что читатель другой, а когда его нет вовсе. Нельзя одного — имитировать анализ: выдуманные альтернативы это ложь в документе, который считают источником правды.
Обратное предупреждение: не всякий скучный ADR процессный — запись, которую через год откроет дежурный, рабочая. Критерий тут читатель и решение, а не ваши ощущения при написании.
Чек-лист ревью чужого ADR
Пять минут и вопросы вместо правок стиля (механика — в «Ревью текста»): по заголовку понятно решение и отвергнутая альтернатива? в контексте есть проверяемые числа и даты? названо, чем пожертвовали, и причина отказа у каждого варианта? есть отрицательные последствия и понятно, что сломается при отмене? есть критерий пересмотра, владелец и ссылка на код? Про тон: ADR — заявление о том, что человек сделал и почему, поэтому критика формулировок читается как критика решения; разделяйте явно — «с решением согласен, спорю с формулировкой» («Обратная связь»).
Типичные ошибки
- Тема вместо решения в заголовке: «ADR-0021: Kafka» не находится поиском и ни на что не отвечает.
- Контекст из прилагательных: «высокая нагрузка» непроверяемо через два года, а значит бесполезно.
- Только положительные последствия: решение без цены — реклама; читатель снимет его, не зная, за что платил.
- Редактирование принятой записи: текст перестаёт описывать и прошлое, и настоящее, но выглядит достоверно. Удаление отклонённых: теряется ответ на ежегодно всплывающий вопрос.
- Запись задним числом и сокрытие нетехнических причин: реконструкция по памяти вымывает неудобные ограничения (сроки, люди, политика), а «выбрали Go, потому что команда его знает» — законная причина, маскировка которой под техническое сравнение вводит читателя в заблуждение.
- Гигантский ADR на всю систему и журнал в вики при коде в git: не ищется и не обновляется.
Практика
- Археология. Найдите в коде место, которое выглядит странно, и восстановите, почему оно такое: blame, PR, чаты, люди. Засеките время — столько стоит отсутствующий ADR, умноженный на число людей, которые пройдут этим путём.
- Ретроспективная запись. Напишите ADR для решения, принятого 3–12 месяцев назад, с пометкой, что запись сделана задним числом: заметьте, сколько контекста потеряно, и перепишите «Контекст» так, чтобы каждое утверждение проверялось через два года.
- Тест на отмену и на ссылку. Попросите коллегу, не участвовавшего в решении, одной фразой
сказать, что сломается при отмене (не ответил за минуту — правьте «Последствия»), затем прогоните
git log --grep 'ADR-' | wc -l: результат покажет, живой у вас журнал или процессный.
Источники
- Michael Nygard, «Documenting Architecture Decisions», 2011: https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions
- adr.github.io — каталог форматов и инструментов: https://adr.github.io/; шаблон MADR: https://adr.github.io/madr/; CLI: https://github.com/npryce/adr-tools
- Olaf Zimmermann, «Y-Statements»: https://medium.com/olzzio/y-statements-10eb07b5a177
- ThoughtWorks Technology Radar, «Lightweight Architecture Decision Records» (Adopt): https://www.thoughtworks.com/radar/techniques/lightweight-architecture-decision-records
- Spotify Engineering, «When Should I Write an Architecture Decision Record»: https://engineering.atspotify.com/2020/04/when-should-i-write-an-architecture-decision-record/
- AWS Prescriptive Guidance, ADR process: https://docs.aws.amazon.com/prescriptive-guidance/latest/architectural-decision-records/adr-process.html
- Joel Parker Henderson, примеры и шаблоны: https://github.com/joelparkerhenderson/architecture-decision-record; Mark Richards, Neal Ford, «Fundamentals of Software Architecture», глава об ADR.
Мини-итог
- ADR пишется для инженера, которого ещё нет в компании, под решение «менять ли то, что стоит». Запись о прошлом не устаревает: «в марте 2024 мы решили» останется правдой навсегда, в отличие от «сервис работает так». Отсюда неизменяемость: правится только статус, остальное — новая запись.
- Против устаревания работает структура, а не призывы: файл рядом с кодом, запись в том же PR,
генерируемый индекс, владелец через
CODEOWNERS, критерий пересмотра по сигналу. Контекст — числа и ограничения, перепроверяемые через два года; последствия обязаны включать цену и запреты, лучше закреплённые линтером; отвергнутые варианты иrejectedэкономят годы обсуждений. - Процессные ADR распознаются тестом на ссылку и тестом на конфликт; лечатся понижением порога, сужением области значимых решений и признанием аудитора как читателя — но не имитацией анализа. Не тянет на полный шаблон — Y-statement в одну фразу: на порядок лучше, чем ничего.
Что дальше
ADR фиксирует уже принятое решение. Но серьёзные решения сначала нужно вынести на обсуждение: собрать возражения, найти тех, кто знает про подводные камни, договориться о критериях выбора — и только потом записывать итог. Для этого есть жанр с другой динамикой: документ, который приглашает спорить и заканчивается решением, а не начинается с него.
RFC и проектные документы: как выносить решение на обсуждение