ADR-0052: Backend conventions hardening — DI naming, HTTP envelope, submodule docs
Status: accepted Date: 2026-05-09 Deciders: команда проекта
Контекст
ADR-0030 ввёл uber-go/fx как обязательный DI runtime и зафиксировал
правило «каждый BC поставляет <bc>.Module() через internal/core/<bc>/di.go».
ADR-0031 ввёл микроядро суб-модулей и обязал каждый суб-модуль иметь
AGENTS.md и README.md. На уровне HTTP в internal/platform/httpx
существует общий пакет с WriteJSON, DecodeJSON, HandleError и
ErrorEnvelope, и в public-api.openapi.yaml зафиксирован canonical
формат ошибки Error / ErrorEnvelope.
На практике дерево разъехалось:
- Имя DI-файла. 18 BC использовали
di.go, 15 —fx.go(cabinet/*, pricing, stock, delivery, search/proposal и др.). Технически это неважно для работы fx, но создаёт ложное впечатление, чтоfx.goиdi.go— разные сорта файлов. Новый инженер каждый раз тратит время, проверяя, не делают ли они разное. - HTTP-helpers. 12+ handler’ов в
core/*имели локальные приватные копииwriteJSON/writeError/writeErrorJSON. Часть возвращала{error: "string"}, часть —{error: {code, message}}. Контракт ошибки в публичном API объявлен один (ErrorEnvelope), на практике клиент получал две-три различных формы. Спека сама зафиксировала это рассогласование, держа параллельную схемуErrorResponseдля одного endpoint’а. - Документация суб-модулей. Большинство суб-модулей (на момент
фиксации — порядка 22) не имели
AGENTS.mdиREADME.md, несмотря на формальное требование ADR-0031. Шаблоны лежат вdocs/docs/20-architecture/templates/{bc,submodule}-{AGENTS,README}.md, но обязательность была неформальной.
ADR-0030 и ADR-0031 формально иммутабельны (правила из самих ADR), поэтому уточнения вводятся отдельным ADR. Этот документ затягивает гайки и ставит CI-проверки.
Решение
Уточняем три конвенции backend-уровня. Все три проверяются автоматически.
1. Имя DI-файла = di.go. Без исключений.
В каждом пакете-композиционном корне (BC, sub-module, platform-подсистема)
файл, который объявляет fx.Module(...) либо Module()/Module-export
поверх fx.Option, называется di.go. Имя fx.go запрещено.
platform/di/ остаётся самим собой как пакет di (платформа уровня).
CI-проверка: новый шаг в Makefile / GitLab pipeline отвергает любой
файл **/fx.go в backend/.
2. HTTP-ответы — только через internal/platform/httpx.
httpx.WriteJSON(w, status, payload)— единственный путь сериализации тела ответа.httpx.HandleError(w, r, err)— единственный путь рендеринга ошибки.httpx.ErrorEnvelope({error: {code, title, message, category, retryable, request_id, trace_id, details}}) — единственный shape ошибки в JSON-ответе.httpx.DecodeJSON(r, target)— единственный путь декодирования request body.
Запрещено объявлять локальные writeJSON / writeError / writeErrorJSON
функции в handler-пакетах. Запрещено использовать
json.NewEncoder(w).Encode(...) напрямую — пиши через
httpx.WriteJSON.
В public-api.openapi.yaml остаётся единственный envelope —
ErrorEnvelope со схемой Error. Альтернативная схема ErrorResponse
({error: string, detail: string, ...}) удаляется как deprecated.
CI-проверка: regexp grep по func writeJSON\b|func writeError\b|func writeErrorJSON\b|json\.NewEncoder\(w\)\.Encode в backend/internal/core/**/*.go падает с ошибкой при ненулевом совпадении.
3. AGENTS.md + README.md обязательны для каждого BC и суб-модуля.
Каждый каталог backend/internal/core/<bc>/ и
backend/internal/core/<bc>/<submodule>/ обязан содержать оба файла,
заполненных по шаблонам из docs/docs/20-architecture/templates/.
Layer-каталоги (domain/, app/, infra/, api/) внутри суб-модуля
не обязаны иметь AGENTS.md / README.md.
CI-проверка: новый шаг отвергает PR, который добавляет каталог BC или суб-модуля без обоих файлов.
Последствия
Плюсы
- Один взгляд на дерево — одна консистентная картина:
di.goвезде, одна форма ошибки везде, одна структура папки везде. - Новый инженер видит в любом suspended BC всё, что ему нужно знать:
AGENTS.md(правила),README.md(что и зачем),di.go(точка сборки). - Контракт ошибок наконец совпадает с тем, что объявлено в OpenAPI; клиент пишет один парсер.
- Линтерные правила предотвращают регресс: невозможно «случайно» внести локальный writeJSON или забыть AGENTS.md в новом суб-модуле.
Минусы
- Перевод всех существующих handler’ов на
httpx.HandleError— breaking change для тестов, которые ассертили старый{error: "string"}shape. Решается одной волной: тесты адаптируются наbody.error.codeв той же миграции. - Новые CI-проверки увеличивают цикл feedback на ~5 секунд.
- Не все суб-модули одинаково описаны — авторы устают писать «формальные» AGENTS.md. Митигация: шаблоны достаточно компактны (≤30 строк), и качество растёт по мере того, как кто-то реально работает с модулем.
Нейтральные последствия
- ADR-0030 и ADR-0031 не редактируются (иммутабельны). Этот ADR трактуется как «уточнение исполнения» поверх обоих.
- Renamed файлы сохраняют git history через
git mv. - Существующие spec-описания, упоминавшие
ErrorResponse, мигрируют наErrorEnvelope. Внешних клиентов на старом shape нет (см. сессию миграции 2026-05-09).
Рассмотренные альтернативы
Оставить fx.go как валидное имя
Не решает проблему когнитивной нагрузки: на смеси двух имён каждый
новый инженер заглядывает оба, чтобы понять, не разное ли. Один
канонический выбор дешевле. di.go выигрывает потому, что ADR-0030 §2
уже его прямо называет.
Сохранить локальные writeJSON / writeError ради «гибкости»
Гибкость иллюзорна: handler’ы её не используют осознанно, разные формы ошибки появились случайно. Каждый раз, когда формат ошибки нужно расширить (добавить trace_id, например), его правят в одном месте — а двенадцать локальных копий остаются на старом shape, и контракт плывёт.
Документация по необходимости
Уже пробовали: 22 суб-модуля без AGENTS.md/README.md на момент
этого ADR. «По необходимости» в реальности означает «никогда».
Ссылки
- ADR-0030 — Runtime DI через uber-go/fx.
- ADR-0031 — Microkernel sub-modules per bounded context.
- ADR-0051 — Public API auth и API-key RBAC.
internal/platform/httpx/— целевой HTTP-helper пакет.docs/docs/20-architecture/templates/{bc,submodule}-{AGENTS,README}.md— шаблоны документации.