← назад к разделу

Каждый проект накапливает десятки выборов: взяли ClickHouse вместо PostgreSQL, события вместо REST, пакетный потребитель вместо Kafka-движка. Пока решение свежее, оно живёт в голове команды. Через год приходит новый разработчик — или вы сами после отпуска — и спрашивает: «почему здесь так сделано?» Если ответ лежит в закрытом треде мессенджера или вообще нигде — решение будут пересматривать заново, с теми же спорами и теми же граблями.

ADR (Architecture Decision Record) — короткий документ, который фиксирует одно архитектурное решение: что решили, в каком контексте и чем заплатили. Не проектная документация и не описание системы — именно конкретный выбор в конкретной ситуации.

Обязательно

Откуда взялся формат

В 2011 году Майкл Найгард описал простой шаблон из пяти частей: заголовок, статус, контекст, решение, последствия. Позже появился MADR — чуть расширенный вариант с явными альтернативами; расшифровку с третьей версии поменяли с «Markdown Architecture Decision Records» на «Markdown Any Decision Records», чтобы формат не считали пригодным только для архитектуры. Ядро у обоих одно — контекст, решение, последствия, — разница в деталях оформления.

Как выглядит ADR

Минимальный рабочий формат — четыре секции:

# ADR-007: События в ClickHouse через Kafka-потребитель, не Kafka-движок

Статус: accepted (2026-06-06)
Заменяет: —

## Контекст

Аналитика заказов переезжает в ClickHouse (ADR-005). События уже идут
через outbox в Kafka. Нужно выбрать способ доставки Kafka → ClickHouse:
потребитель в сервисе, Kafka-движок ClickHouse или Debezium-пайплайн.

## Решение

Пакетный потребитель в сервисе заказов.

## Альтернативы

- Kafka-движок ClickHouse: нет прикладного кода, но повторные попытки,
  маппинг и алерты уезжают в DDL, который команда сервиса не видит
  и не ревьюит.
- Debezium CDC: зеркалит строки, а нам нужны доменные события.

## Последствия

+ Маппинг и повторные попытки в кодовой базе сервиса, под обычным ревью и тестами.
+ Идемпотентность контролируем сами (ReplacingMergeTree по event_id).
− Потребитель — ещё один компонент сервиса: deployment, мониторинг lag.
− При смене схемы события правим и потребитель, и таблицу.

Для кого это написано

От ответа зависит, можно ли писать названия инструментов без расшифровки, — и ответ такой: для инженера, который придёт в проект через год и не участвовал в обсуждении. Не для руководства, не для внешнего аудита, не для себя-вчерашнего.

Из этого адресата выводятся все правила языка:

  • Термины своей области не расшифровывают. «Взяли генератор запросов вместо ORM», «идём через outbox», «ключ раздела — идентификатор пользователя» — читатель-инженер это знает, а объяснения раздувают текст и делают его нечитаемым для того, кому он адресован. Расшифровывают только внутреннее: имена своих сервисов, внутренние сокращения, названия команд.
  • Пишут на языке команды. Если команда говорит по-русски — запись по-русски, с латинскими названиями технологий как есть. Смешанный язык («задеплоили consumer в prod») лучше заменить на нормальный русский, но переводить названия инструментов не надо.
  • Никаких оценок без опоры. «Решение современное», «так правильнее», «команда согласна» — не аргументы. Аргумент — число, факт про систему или названное ограничение.
  • Прошедшее время и утвердительно. «Выбрали пакетный потребитель», а не «предлагается рассмотреть возможность». Запись фиксирует состоявшееся решение, а не предлагает.

А если читателем будут смежники или аудит? Это другой жанр: для внешнего читателя пишут отдельное краткое пояснение или раздел в общем документе, а не переписывают запись. Смешивание аудиторий — самая частая причина, по которой запись превращается в десятистраничное сочинение: автор пытается объяснить всё всем сразу.

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

Статусы и жизненный цикл

ADR проходит через несколько статусов: proposed → accepted → superseded или deprecated. Есть ещё rejected — для предложения, которое обсудили и не приняли. Его чаще всего забывают, а зря: отклонённый вариант, не оставшийся в истории, через год кто-нибудь принесёт заново — ровно та беда, ради которой ADR и заводят.

proposed accepted superseded новый ADR приняли заменили ссылка rejected отклонили deprecated отменили

Рабочий путь идёт по верхнему ряду слева направо, а вниз уходят два тупика, из которых запись уже не возвращается.

Дата в шапке — дата принятия решения, а не последней правки файла. Это важно именно потому, что решение читают годы спустя: по дате видно, в каком мире его принимали — какие были нагрузки, какие версии, что тогда ещё не вышло.

Важное правило: ADR не редактируют задним числом. Если решение изменилось — пишется новый документ, старый получает статус superseded со ссылкой на новый. История споров остаётся читаемой, а не затирается.

Нумерация сквозная и навсегда: ADR-007 не переиспользуется, даже если решение отменено. Это позволяет ссылаться на конкретный документ и всегда найти его по номеру.

Статус proposed удобен при обсуждении: ADR создаётся ещё в pull request, пока решение не принято. Это лучший способ обсудить развилку до того, как написаны тысячи строк кода.

Решение попробовали и откатили

Отдельный случай, которого нет в стандартном жизненном цикле, а в жизни он встречается чаще смены решения: решение приняли, реализовали, через месяц откатили. Что делать?

Правильный ответ: новая запись, старая получает статус superseded со ссылкой. Причём новая запись — самая ценная из всех, потому что в её контексте лежит то, чего нельзя было знать заранее: что именно пошло не так на практике.

# ADR-019: Возврат к пакетному потребителю вместо Kafka-движка

Статус: accepted (2026-08-14)
Заменяет: ADR-012

## Контекст

В ADR-012 перешли на Kafka-движок ClickHouse. За полтора месяца работы
выяснилось то, чего не было в прикидке: при ошибке разбора сообщения
движок останавливает поток целиком (потеряли 40 минут аналитики
17 июля), а диагностика идёт только через журнал сервера базы,
к которому у команды сервиса нет доступа.

## Решение

Возврат к пакетному потребителю в сервисе (как было до ADR-012).

## Последствия

+ Ошибочное сообщение уходит в недоставленные, поток не встаёт.
+ Диагностика в журнале сервиса, доступна дежурному команды.
− Снова свой код маппинга и повторов, как описано в ADR-007.
− Две недели работы на переход туда и обратно.

Чего не делают: не правят старую запись («мы передумали»), не удаляют её и не пишут «решение оказалось неверным» без объяснения. Старая запись — свидетельство того, что на тот момент решение было разумным при известных тогда данных; ценность — именно в разнице между тогдашним контекстом и новым знанием.

И одна практическая мелочь: в новой записи стоит явно сказать, что изменилось в знании, а не в мнении. «Выяснилось, что…» — хорошая формулировка; «решили, что всё-таки лучше…» — плохая, потому что не даёт следующему читателю ничего, кроме факта колебаний.

Как выглядит плохая запись

Ошибки из списка ниже быстрее понять на примере. Вот запись, в которой нарушено почти всё:

# ADR-004: Kafka

Статус: accepted

## Решение

Решили взять Kafka, это современный и масштабируемый брокер.

## Последствия

Стало лучше, система готова к росту нагрузки.

Что здесь не так, по строкам: нет контекста — непонятно, какую задачу решали и что вообще выбирали; нет альтернатив — через год кто-нибудь принесёт RabbitMQ, и спорить придётся с нуля; нет чисел — «масштабируемый» не аргумент, а «6000 сообщений в секунду на пике и три независимых потребителя» аргумент; нет минусов — значит, это реклама, и читатель не узнает, что за брокером придётся следить и что приоритетов в нём нет; нет даты — непонятно, в каком мире решали; и заголовок называет технологию, а не решение — по списку заголовков невозможно понять, о чём запись.

Та же запись, переписанная: заголовок «ADR-004: Kafka для потока событий уведомлений»; контекст с числами и перечнем рассмотренных вариантов; решение одной строкой; альтернативы с причиной отказа; последствия с двумя минусами. Объём вырастет с восьми строк до тридцати — и запись начнёт работать.

Где хранить

В репозитории, рядом с кодом, который решение описывает: docs/adr/ADR-007-clickhouse-pipeline.md. Решения платформенного уровня — границы сервисов, общие контракты — хранят в архитектурном репозитории рядом с картой сервисов.

В вики ADR умирают: их не ревьюят вместе с кодом и не находят при чтении репозитория.

Когда писать ADR

Писать, когда решение дорого отменить и неочевидно без контекста: выбор хранилища, способа интеграции, формата событий, стратегии миграции, отказ от чего-то ожидаемого («не используем этот ORM», «не версионируем этот API»).

Хорошая проверка: если на ревью или в чате возник спор длиннее десяти сообщений — итог спора стоит ADR.

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

Типичные ошибки

ADR по памяти через год. Решили «задокументировать всё, что есть» и написали двадцать ADR постфактум. Контекст уже утерян, получились описания, а не решения. ADR пишется в момент выбора.

ADR без альтернатив. «Решили взять X» — а что отвергли и почему? Без отвергнутых вариантов следующий человек принесёт их снова.

Правка вместо superseded. Тихо переписали ADR-003 под новое решение — история умерла, ссылки на старое врут.

ADR как длинное сочинение. Десять страниц с диаграммами — это проектный документ, не ADR. Решение должно читаться за пару минут; детали — по ссылкам.

Решение есть, ADR «потом». «Потом» не наступает. ADR входит в критерии готовности той задачи, где развилка решилась.

Дополнительно: при первом чтении можно пропустить

Глубже: как найти нужную запись среди сорокарасширенное

Путь к файлу — только половина ответа. Через два года записей сорок, и без навигации они перестают читаться, даже если каждая хороша.

Индекс — обязателен, и он один файл. docs/adr/README.md со строкой на запись: номер, заголовок, статус, дата. Пять минут работы при добавлении записи и единственное, что делает набор записей пригодным для чтения.

| № | Решение | Статус | Дата |
|---|---|---|---|
| 001 | Монолит до первых десяти команд | accepted | 2025-03-11 |
| 004 | Kafka для потока событий уведомлений | accepted | 2025-06-02 |
| 012 | Kafka-движок ClickHouse для аналитики | superseded (→019) | 2026-06-06 |
| 019 | Возврат к пакетному потребителю | accepted | 2026-08-14 |

Что этот индекс даёт, кроме поиска: по нему видно заменённые решения (и не надо открывать файл, чтобы понять, актуально ли), и по нему читают историю проекта подряд — это лучшее чтение для нового человека в команде, лучше любого обзора архитектуры.

Ссылки друг на друга. Запись почти всегда опирается на предыдущие («аналитика переезжает в ClickHouse — ADR-005»), и эти ссылки стоит ставить. Тогда от любой записи можно дойти до связанных, и получается граф решений, а не куча файлов.

Что искать в самих записях. Поиск по репозиторию работает, если в заголовках стоят осмысленные слова, а не только названия технологий: «ADR-012: Kafka-движок ClickHouse для аналитики» находится и по «аналитика», и по «ClickHouse». Плюс полезно держать в шапке строку с метками: теги: аналитика, брокер, clickhouse.

Инструменты. Они нужны реже, чем кажется, но знать о них стоит. adr-tools — набор команд, который создаёт файл по шаблону с автонумерацией и сам правит индекс (то есть закрывает ровно ту работу, которую забывают). log4brains собирает из тех же файлов статический сайт с поиском и хронологией — удобно, когда записей много и их читают не только из редактора. Плюс шаблон в репозитории (docs/adr/template.md), который копируют руками, — минимальный вариант, работающий всегда. Порядок внедрения: шаблон и индекс сразу, инструмент — когда записей станет тридцать и ручное обновление индекса начнёт забываться.

Глубже: решение на три сервиса: где его хранитьрасширенное

Самый частый практический вопрос, который правило «рядом с кодом» не закрывает: решение затрагивает три сервиса из трёх репозиториев. Куда?

Признак, по которому выбирают: кто владеет решением и кто будет его читать.

  • Решение внутри одного сервиса (как устроен его потребитель, какое хранилище у его данных, какой формат его внутренних сообщений) — в репозитории этого сервиса. Читают его те, кто работает с этим кодом.
  • Решение уровня платформы (формат событий, общий для всех; способ аутентификации между сервисами; правило владения данными; границы сервисов; общий формат идентификаторов) — в отдельном архитектурном репозитории, рядом с картой сервисов и реестром интеграций. Читают его несколько команд, и ни один сервисный репозиторий не является для него правильным домом.

Как это не разъезжается, механикой. Три вещи, которых обычно не хватает:

  1. Сквозная нумерация внутри каждого хранилища, с префиксом. ARCH-014 для платформенных и ADR-007 для сервисных — тогда по номеру видно, где искать, и номера не конфликтуют.
  2. Ссылка из сервиса на платформенное решение. В сервисном репозитории при реализации платформенного решения ставят короткую запись: «формат событий — по ARCH-014, здесь особенности такие-то». Дублировать содержимое не надо, а ссылка нужна: иначе разработчик сервиса о решении не узнает.
  3. Явный владелец платформенного решения. У сервисной записи владелец очевиден (команда сервиса), у платформенной — нет, и без имени она не обновляется и не исполняется. Обычно это архитектор или ведущий инженер платформы, и это его работа.

Чего стоит избегать: копии одной записи в трёх репозиториях (разъедутся через месяц) и платформенных решений в репозитории «самого главного» сервиса (через год этот сервис распилили, а решения остались в чужом коде).

Глубже: от записи к проверкерасширенное

Запись фиксирует решение — и не заставляет его соблюдать. Это тот самый разрыв, из-за которого решение «живёт два спринта»: новый человек не читал, старый забыл, а код спокойно уехал в другую сторону.

Мостик от записи к исполнению строят в три ступени, и стоит выбирать самую дешёвую из достижимых.

Ступень 1: запись — источник, а не финал. В последствиях записи явно называют, что теперь должно быть верно в коде: «все доменные события публикуются через таблицу исходящих», «прямые вызовы между сервисами А и Б запрещены», «новые таблицы — только с идентификатором седьмой версии». Формулировка в виде проверяемого утверждения — уже половина дела, потому что её можно проверить глазами на ревью.

Ступень 2: проверка в конвейере. Многие такие утверждения проверяются машиной: тест архитектуры на запрещённые зависимости между пакетами, правило линтера на запрещённый вызов, проверка миграций на тип идентификатора, тест на отсутствие прямого обращения к чужой таблице. Правило работает не потому, что его помнят, а потому, что сборка краснеет. Именно это отличает решение, которое живёт, от решения, которое записали.

Ступень 3: то, что машиной не проверить — в чек-лист ревью. «Новый эндпоинт идемпотентен», «событие несёт время события» — такие вещи остаются на человеке, и их место в коротком списке вопросов ревьюера, а не в памяти.

Практическое правило, замыкающее жанр: у записи, вводящей правило, в последствиях стоит строка «чем проверяется» — имя теста, правило линтера, шаг конвейера или пункт чек-листа. Если в этой строке написано «внимательностью команды», решение проживёт ровно до первого нового человека. Подробнее про превращение договорённостей в проверки — в статье про роль архитектора.

Коротко

  • ADR — короткий документ об одном архитектурном решении: что решили, почему, какие были альтернативы и чем заплатили.
  • Формат: контекст + решение + альтернативы + последствия (с плюсами и минусами).
  • Статусы: proposed → accepted → superseded / deprecated, плюс rejected для отклонённых предложений; старый ADR не редактируется, пишется новый.
  • Хранить рядом с кодом в docs/adr/, не в вики — иначе не найдут и не ревьюят.
  • Писать в момент выбора, не постфактум; вносить в критерии готовности задачи. Не писать на стандартные решения, обратимые мелочи и очевидное из кода.
  • Адресат записи — инженер, который придёт через год: термины области не расшифровывают, внутренние сокращения расшифровывают, оценки без опоры не годятся, время прошедшее и утвердительное.
  • Откатили решение — пишут новую запись со ссылкой на старую, и её контекст самый ценный: там лежит то, что выяснилось только на практике; старую не правят и не удаляют.
  • Набор записей держится на индексе одним файлом (номер, заголовок, статус, дата), ссылках друг на друга и осмысленных заголовках; инструменты подключают, когда записей тридцать.
  • Решение внутри сервиса живёт в его репозитории, платформенное — в архитектурном, с префиксом в нумерации, ссылкой из сервиса и явным владельцем; копий в трёх репозиториях не делают.
  • В последствиях записи, вводящей правило, стоит строка «чем проверяется»: тест архитектуры, правило линтера, шаг конвейера или пункт чек-листа — иначе решение живёт до первого нового человека.

Что почитать дальше