Привет, Хабр! Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.

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

Проблема в том, что в большинстве компаний долг измеряют качественно («кажется, стало больнее») или вообще игнорируют до пожара. А нужно — количественно, с автоматическими порогами и архитектурными тестами, которые не дадут выродиться системе в «большой комок грязи» (Big Ball of Mud).

В этой статье мы поговорим о том, как отличить полезный компромисс от смертельной петли, измерить трение в архитектуре и автоматически управлять техдолгом через фитнесс‑функции.

Диагностика: почему больно и где именно

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

Вы рисуете график (мысленно или в Prometheus). По оси X — спринты или недели. По оси Y — человеко-часы на реализацию типовой пользовательской истории (например, «добавить новое поле в отчет» или «создать новый тип платежа»).

В здоровой системе график идёт горизонтально или чуть вверх. В системе с критическим архитектурным долгом он сначала пологий, а потом взлетает по экспоненте. Перегиб — момент, когда проценты по долгу съедают весь запас по времени выхода на рынок.

Две метрики из мира DORA (DevOps Research and Assessment — это набор показателей для оценки эффективности доставки программного обеспечения и зрелости DevOps‑процессов), которые работают лучше любого code review. Посчитать их можно следующим образом.

Возьмите два числа за последние 30 дней:

  • Lead Time for Change (LTFC) — время от пулл‑реквеста до продакшена. Измеряется в часах/днях.

  • Change Failure Rate (CFR) — процент релизов, вызвавших инцидент, откат или хотфикс.

Здоровый диапазон для высокоэффективной команды: LTFC < 1 часа, CFR < 15%. Архитектурный долг себя выдаёт так:

  • LTFC растёт, CFR остаётся низким → система стала слишком хрупкой, каждый PR требует ручного тестирования.

  • LTFC растёт, CFR тоже растёт → классическая спираль смерти. Разработчики боятся менять код, делают костыли, костыли ломают ещё что‑то.

  • LTFC низкий, CFR высокий → вы быстро катите, но всё ломается. Долг в тестах или в отсутствии изоляции компонентов.

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

Как найти конкретные точки трения, а не просто констатировать факт

Метрики уровня команды нужны для триажа. Триаж в IT — это процесс первичной оценки, сортировки и приоритизации уязвимостей, инцидентов или алертов безопасности. Его цель — быстро определить критичность угроз и сосредоточить ресурсы на наиболее опасных из них, особенно когда поток данных или ресурсов ограничен.

Возьмём типовой симптом: для добавления одного поля в объект Order вам пришлось править 12 файлов в 5 разных модулях. Почему? Потому что модель данных не локализована, а размазана по слоям через самодельную ORM или общий DTO‑контракт.

Проведите простой эксперимент (ручной, но эффективный):

  1. Выберите одну бизнес‑сущность (User, Payment, Shipment).

  2. Найдите все файлы в репозитории, где она упоминается (grep + сортировка по пути).

  3. Посчитайте количество уникальных директорий/модулей, затронутых этой сущностью.

Если число больше трёх‑четырёх — перед вами архитектурный долг со знаком минус. Потому что сущность стала глобальной, а должна быть скрыта за доменным интерфейсом.

Диагностический чек‑лист для архитектора

Прежде чем начинать лечить непонятное, поймите тип долга. Вот три основных клинических случая.

Случай А: Долг связности (Coupling debt). Изменение в модуле А гарантированно ломает модули Б, В и Г. Причина: общие таблицы в БД, общие DTO через границы сервисов, синхронные цепочки вызовов. Диагностика: собрать трассировку одного бизнес‑сценария в Jaeger. Если глубина вызовов > 5, а больше половины из них идут в одну базу — диагноз подтверждён.

Случай Б: Долг отложенного дизайна (Design debt). Архитектура есть, но она не соответствует реальным паттернам использования. Например, вы спроектировали event‑driven систему, но 90% трафика — это синхронные запросы, которые превращают Kafka в дорогую очередь. Диагностика: соотношение асинхронных сообщений к синхронным вызовам по одному потоку данных. Перекос > 10:1 в пользу синхронизации означает, что вы платите за инфраструктуру, которая вам не нужна.

Случай В: Долг эволюции (Cruft). Система росла, требования менялись, но никто не пересматривал архитектурные решения (ADR). Появились «мёртвые зоны» — фичи, которые никто не использует, но которые требуют миграций данных и поддержки кода. Диагностика: профилирование вызовов в продакшене (например, через continuous profiling инструменты вроде Pyroscope). Функции или эндпоинты с нулевым трафиком за месяц, но с высоким временем сборки — главные кандидаты на удаление. 

Решение: не переписывать, а выплачивать долг процентами

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

Правильная стратегия — лечить боль, а не симптомы. То есть, вы не обязаны починить всё, вы обязаны убрать бутылочное горлышко, которое блокирует развитие.

Пошаговый алгоритм для каждого типа долга

  • Для долга связности (случай А): внедряйте антикоррупционные слои (Anti‑Corruption Layer, ACL) не на границе монолита, а на границах доменов внутри монолита.

Пример на псевдокоде: до рефакторинга у вас везде напрямую используется OrderDbModel из общей БД. Везде, где нужен заказ, тянут Hibernate‑сущность.

После: создаёте модуль order.contract с интерфейсом OrderRepository и примитивной моделью Order. Внутри своего домена работаете только с ней. Адаптер к старой БД живёт в отдельной папке. Теперь, когда вы решите вынести заказы в микросервис, меняется только адаптер. Ни один файл за пределами order.* не узнает об этом.

  • Для долга отложенного дизайна (случай Б): откатывайте инфраструктуру там, где это возможно. Если синхронных вызовов в 20 раз больше, чем асинхронных, перестаньте гнать всё через брокер. Переключите прямые вызовы на HTTP с circuit breaker, а брокер оставьте только для фоновых задач, где задержка в 100 мс не критична. Это не регресс — это возврат к адекватной архитектуре.

  • Для долга эволюции (случай В): удаляйте код, а не комментируйте. Внедрите правило «Feature Toggle с датой удаления». Каждый тогл, который живёт дольше трёх месяцев, автоматически создаёт тикет на удаление кода. Метрика: если код не вызывается в проде за 14 дней и на него нет интеграционных тестов — он идёт в помойку. Страх «а вдруг пригодится» лечится через git history.

Архитектурная фитнес функция как автопилот долга

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

Фитнес функции — это тест, который проверяет не корректность логики, а соблюдение архитектурных ограничений. Идея взята из книги «Building Evolutionary Architectures» (Ford, Parsons, Kua).

Для реализации создается отдельный модуль с тестами, которые запускаются в CI на каждый PR. Используйте готовые инструменты или пишите свои проверки.

Пример для Java с ArchUnit (можно адаптировать под любой язык с рефлексией):

@Test
void domain_should_not_depend_on_infrastructure() {
    // Запрещаем доменному слою импортировать что-либо из БД, HTTP-клиентов или фреймворка
    noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat().resideInAnyPackage("..jpa..", "..httpclient..", "..spring..")
        .check(importedClasses);
}

@Test
void order_module_should_expose_only_interfaces() {
    // Модуль заказов должен экспортировать наружу только интерфейсы и DTO,
    // но не реализации репозиториев и сервисов
    classes().that().resideInAPackage("..order..")
        .should().haveSimpleNameEndingWith("Impl")
        .andShould().haveModifier(Modifier.PUBLIC)
        .check(importedClasses);
}

Пример для модульности на уровне сборки (Gradle или Bazel): запрет циклических зависимостей между модулями. Проверяется через команду gradle modules ‑scan или через jQAssistant.

Пороги, которые не дают умереть

Недостаточно просто написать фитнес функцию, нужно задать уровни.

  1. Зелёный: всё хорошо. Новый код укладывается в правила.

  2. Жёлтый: предупреждение. Например, цикломатическая сложность модуля выросла с 8 до 11 при лимите 10. CI пропускает, но генерирует отчёт. Команда должна разобрать это на ретроспективе.

  3. Красный: блокировка слияния. Если нарушена архитектурная граница (например, UI‑слой полез в БД напрямую) или превышен порог технологического долга (количество публичных классов в модуле больше 50 — признак, что модуль надо делить).

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

Как мы платили проценты по долгу и не обанкротились

Рассмотрим обобщённый случай из жизни. Платформа онлайн‑обучения, на которую потрачены три года разработки. Монолит на Django, который вырос до 150 тысяч строк. Проблема: добавление нового типа контента (например, интерактивный квиз вместо видео) требовало правки в 12 файлах: модели, схемах API, админке, вьюхах, шаблонах, signals, celery‑тасках.

LTFC вырос до 5 дней. CFR — 34%. Команда, естественно, устала.

Для диагностики проблемы построили граф импортов в модуле content. Оказалось, что quiz/models.py импортирует video/models.py, а тот — quiz/utils.py. Зацикливание.

Первая фитнес-функция содержит запрет циклических импортов на уровне модулей. Красная зона для PR, где новый импорт замыкает цикл.

Вторая: ограничение на количество файлов, которые трогает один PR по добавлению новой фичи (сбор через diff‑статистику в CI). Предел — 10 файлов. Если лимит превышен, значит, сущность слишком размазана.

Для решения выделили content.contract — модуль с интерфейсами ContentProcessor, ContentRenderer, ContentAdmin. Каждый тип контента (видео, квиз, статья) стал подключаться через DI, а не через прямые импорты. Старый код три месяца работал параллельно через адаптер‑прокси, который направлял вызовы либо к старой логике, либо к новой в зависимости от Feature Toggle.

Через полгода старые модели были удалены. Импорты остались только через contract.

Результат через три месяца: LTFC упал с 5 дней до 4 часов. CFR — с 34% до 12%. Количество файлов на типовой PR сократилось с 12 до 3–4.

Что делать в итоге

В заключении рассмотрим три шага, не требующих одобрения архитектурного комитета и бюджета на переписывание всего.

  • Шаг 1. Зафиксируйте боль на графике. Возьмите три фичи, которые команда сделала за последний месяц. Посчитайте человеко‑часы на каждую. Сравните с аналогичными фичами годичной давности. Если рост больше 50% — у вас есть подтверждённый архитектурный долг, а не просто ощущение.

  • Шаг 2. Внесите первую функцию в CI. Начните с малого: запретите циклические зависимости между пакетами (в Java это ArchUnit, в Python — pytest with import‑linter, в Go — go mod graph + grep). Это займёт два часа и остановит самый опасный вид долга — когда все зависит от всего.

  • Шаг 3. Выберите одну «горячую» сущность и изолируйте её за антикоррупционным слоем. Не чините всё. Возьмите ту фичу, которая чаще всего вызывает баги и долгие PR. Сделайте вокруг неё фасад. Переключите на него один, самый простой сценарий. Промерьте LTFC через неделю. Если стало легче — масштабируйте подход.

Умение замечать архитектурный долг начинается с понимания архитектурных принципов и компромиссов. Для самопроверки можно пройти вступительный тест курса «Архитектор программного обеспечения» и посмотреть, какие темы стоит изучить глубже.

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

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

  • 17 июня, 20:00. «Архитектура информационных систем. Монолиты, SOA и микросервисы». Записаться.
    Разберём сильные и слабые стороны разных архитектурных подходов и критерии выбора архитектуры под конкретные задачи.

  • 8 июля, 20:00. «Чистая архитектура на Go без “карго-культа”: слои, DTO и интерфейсы». Записаться
    Разберём, как проектировать границы модулей и избегать избыточной связности в приложениях.

Полный список бесплатных уроков июня смотрите в дайджесте.