Тимлид и инженерное лидерство Технический долг как управленческая задача
0%

Технический долг как управленческая задача

Технический долг как управленческая задача

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

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

Сразу о качестве доказательств

Управление долгом — область, где уверенных цифр почти нет, а презентаций с цифрами очень много. Разделим измеренное и рассказанное. Что можно считать опорой:

  • Метафору ввёл Уорд Каннингем на OOPSLA'92 (текст). В видео 2009 года он уточнил: речь была не про «писали грязно и теперь платим», а про разрыв между кодом и текущим пониманием домена. Долг растёт не только от спешки, но и от того, что вы узнали новое о предметной области.
  • Законы эволюции ПО Лемана (1980, Programs, Life Cycles, and Laws of Software Evolution): используемая система обязана меняться, а её сложность растёт, пока в неё специально не вкладываются. Долг — состояние по умолчанию, а не следствие плохой команды.
  • Tornhill & Borg, «Code Red: The Business Impact of Code Quality» (TechDebt'22, 39 проприетарных кодовых баз): в коде худшего качества дефектов примерно в 15 раз больше, задачи занимают около вдвое больше времени, разброс сроков — до 9 раз выше. Ограничения: исследование наблюдательное, метрика Code Health проприетарная, авторы связаны с продуктом CodeScene. По порядку величины — лучшее, что есть.
  • Besker, Martini, Bosch (2019, Journal of Systems and Software): разработчики сообщают, что теряют из-за долга около четверти времени — самооценка в опросах, а не замер. Рядом Potdar & Shihab (2014): TODO/FIXME встречаются в 2,4–31% файлов; дешёвый сигнал, не оценка размера.
  • Google, «Defining, Measuring, and Managing Technical Debt» (IEEE Software, 2023): самый полезный результат отрицательный — ни одна из проверенных объективных метрик не предсказывает того, что инженеры сами считают проблемным. Лучшим источником остались структурированные опросы команд.

Что цифрами не является, хотя выглядит как цифры: оценки консалтинга вроде «20–40% стоимости технического ландшафта — долг» (McKinsey, 2020) и вендорские опросы вроде Stripe Developer Coefficient («33% времени», 2018). Методика не раскрыта, выборка самоотбором, у авторов коммерческий интерес. Использовать можно — но честно, как отраслевое ощущение: принесёте такое как факт, поймают один раз — доверия к вашим оценкам не останется. Всё остальное ниже — обобщённая практика, у которой видно, как выглядит нарушение и чем оно кончается.

Определение, которое работает у руководителя

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

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

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

Три теста «это долг или вкусовщина»

Инженер приходит с «тут надо переписать». Прогоните три вопроса вслух, при нём.

Вопрос Если ответа нет
Кто и как часто платит проценты? Назови три последних случая с датами Это эстетика. Уважайте вкус, но не тратьте на него время команды
Что из ближайшего плана станет дороже или невозможно? Долг есть, но он не срочный — в реестр, без даты
Сколько стоит починить и как мы поймём, что закончили? Это не задача, а настроение: без критерия «готово» работа не кончается никогда

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

Долг — портфель рисков, а не список задач

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

Разбор квадрантов — интуиция здесь ошибается:

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

Отдельно про «секрет в конфиге»: безопасность этой матрицей не приоритизируется вообще — отдельный список с жёстким сроком, см. security в пайплайне.

Что ломается чаще всего

Квота «20% на техдолг» превращается в ноль

Заявленная квота на техдолг сжимается за квартал до нуля

Механика деградации одинаковая: квота объявлена в долях, а не в пунктах; у доли нет владельца; в каждом отдельном спринте находится разумная причина занять её продуктовой работой; через квартал сумма равна нулю, и никто этого не заметил, потому что сумму никто не считал. Что помогает: квота записывается в пунктах — не «20%», а «в этом квартале: тесты на расчёт тарифов, удаление второго парсера, автоматизация ручного шага в релизе», у каждого владелец и критерий «готово»; планируется первой, а не остатком — работу «если останется время» не берут никогда; расход считается вслух раз в спринт строкой «из трёх пунктов сделан один, два перенесены, причина — инцидент» (про отчётность вверх — глава 15); перенос стоит явного решения — не «подвинем», а «переносим пункт X, потому что Y, новая дата Z». Три переноса подряд означают, что пункт не приоритетен: верните его в реестр честно.

«Мы всё перепишем»

Переписывание привлекательно тем, что не требует разбираться в существующем коде, — и в этом же главная опасность: старый код содержит годы неявных требований, которых нет ни в одном документе. Аргументы против опираются на опыт, а не на исследования: Джоэл Спольски о переписывании Netscape (2000) — разбор одного случая, не доказательство; показательно и то, что Брукс в юбилейном издании «Мифического человеко-месяца» отказался от совета «планируйте выбросить первую версию» в пользу инкрементального развития. Практическое правило: переписывание оправдано, когда старое можно выключить целиком и сразу, а это почти никогда не так. Иначе работает удушающая замена (strangler fig): новый код берёт часть трафика за фасадом, старый отмирает по кускам — дороже в моменте, зато каждая неделя даёт показываемый результат и проект можно остановить, не потеряв всё. Три вопроса до старта: что мы выключим и какого числа (нет ответа — вы не переписываете, а создаёте вторую систему); кто платит за двойную поддержку и сколько это в людях; что сделаем, если через два месяца станет ясно, что не успеваем (ответ «поднажмём» означает, что плана нет).

Долг живёт в отдельном бэклоге, который никто не открывает

Симптом: проект TECHDEBT на 400 тикетов, последняя активность полгода назад. У списка нет ритуала пересмотра и связи с планами продукта, поэтому он становится кладбищем и — хуже — создаёт иллюзию управления («мы же ведём учёт»). Лечится тремя правилами: реестр короткий (десятки пунктов, не сотни); пересматривается раз в квартал вместе с планированием продукта; в нём записаны проценты, а не описания — «legacy-код в модуле отчётов» бесполезно, а «любое изменение схемы отчёта требует правки в двух местах и ручной проверки: 2 дня на задачу, в плане квартала 4 такие» работает. Механика — работа с бэклогом.

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

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

Два перекоса: долг как универсальное объяснение и ремонт только по пути

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

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

Окупаемость: почему «починим и станет быстрее» — не аргумент

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

Кривые накопленных затрат с точкой окупаемости рефакторинга

  • Окупаемость зависит от оставшегося срока жизни кода, а не от того, насколько он плох. Первый вопрос — не к команде, а к продукту: «сколько ещё месяцев мы планируем менять этот модуль»; ответ «до конца года выключаем» означает решение «не чинить».
  • До точки окупаемости вы выглядите хуже, чем без вложения: команда поставляет меньше. Объявите это заранее — иначе через месяц спросят, почему упала скорость, и разговор проигран.
  • Обе кривые — оценки. Мартин Фаулер в «Is High Quality Software Worth the Cost?» утверждает, что период окупаемости измеряется неделями, и оговаривается: твёрдых данных нет, это аргумент из опыта. Не выдавайте его за расчёт.

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

Как измерять, не обманывая себя

Помня отрицательный результат Google: метрики — не приговор, а повод посмотреть.

Прокси-метрика Что показывает Чего не видит
Горячие точки (частота изменений × размер файла) Где ремонт вернётся быстрее всего Структурный долг, размазанный по многим файлам
Доля файлов с одним автором Знаниевый риск, bus factor Это долг команды, а не кода, — но платить всё равно вам
Время пайплайна, доля флаки-тестов Прямой налог на каждую задачу То, что пайплайн вообще не проверяет
DORA: lead time, change failure rate, MTTR Здоровье потока доставки Причину: метрика ухудшилась — почему, не скажет
Доля незапланированной работы Сколько система забирает без спроса Долг, который ещё не выстрелил
Опрос команды раз в квартал (5 вопросов) Лучший из доступных источников Субъективность, эффект недавних событий

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

"""Доля работы, которую забирает сама система. Вход: CSV из трекера с колонками
id, category, days, closed_at; category: feature|debt|incident|support|unplanned."""
import csv
from collections import defaultdict
from datetime import date

# Категории «работы из-за системы», а не «работы над продуктом»
SYSTEM_TAX = {"debt", "incident", "support", "unplanned"}


def load(path: str) -> dict[str, dict[str, float]]:
    """{квартал: {категория: чел-дни}}. O(n) времени, O(k) памяти."""
    acc: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
    with open(path, encoding="utf-8") as f:
        for row in csv.DictReader(f):
            d = date.fromisoformat(row["closed_at"])
            acc[f"{d.year}-Q{(d.month - 1) // 3 + 1}"][row["category"]] += float(row["days"])
    return acc


for quarter, cats in sorted(load("tasks.csv").items()):
    total = sum(cats.values())
    tax = sum(v for k, v in cats.items() if k in SYSTEM_TAX)
    print(f"{quarter}: {total:.0f} чел-дней, налог системы {tax / total:.0%}")

Три правила обращения с этим числом. Ценность — в тренде: «налог системы» вырос с 28% до 41% за три квартала — это разговор, одна цифра без ряда не значит ничего. Категория проставляется при закрытии задачи, а не задним числом, иначе вы измеряете память команды. И не спорьте о точности: услышите «данные грязные» — согласитесь и покажите, почему направление всё равно видно; требование идеальных данных — стандартный способ утопить неудобный разговор.

Решение по конкретному пункту

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

Два узла пропускают чаще всего. «Удалить код и фичу»: посмотрите в аналитику, кто пользуется функциональностью — регулярно выясняется, что спорный модуль обслуживает 0,3% пользователей и вместо трёх недель ремонта нужен один разговор об отключении; самый дешёвый вид ремонта почти никогда не рассматривается, потому что удаление ощущается как потеря. «Изолировать и страховать»: фасад, ограничение доступа, мониторинг, описанная процедура восстановления — риск падает за дни, а не за месяцы (подробности — паттерны устойчивости и наблюдаемость).

Жизненный цикл пункта долга

Долг без явных состояний живёт в двух режимах: «все знают» и «никто не помнит». Явный цикл даёт третий — «решение принято и записано».

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

Где взять время: четыре способа и их цена

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

Способ Как выглядит Цена и когда ломается
Внутри оценки задачи Уборка по пути включена в задачу Не берёт структурный долг; исчезает первым при давлении на сроки; злоупотребление разрушает доверие к оценкам
Именованные пункты в плане квартала 2–4 пункта с владельцами и критериями Требует согласования и умения объяснять; единственный устойчивый вариант
Отдельная итерация («неделя качества») Раз в квартал команда занимается только долгом Хороша для накопленной мелочи, бесполезна для крупного; отменяется первой же горящей задачей
Условие в «определении готового» Фича не готова без тестов и без удаления старого пути Сильнейший механизм против нового долга и слабейший против старого; работает, только если вы не делаете исключений

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

Разговор с продуктом

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

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

Формулировка, которую стоит выучить дословно:

«Я не прошу время на качество. Я показываю, что четыре задачи из плана стоят дороже, чем мы думали, и предлагаю сделать их дешевле. Можем не делать — тогда декабрьская интеграция занимает не три недели, а пять; я хочу, чтобы это было общим решением, а не сюрпризом.»

Про эскалацию и отчётность вверх — глава 15.

Сознательный долг: как выписывать расписку

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

# docs/decisions/2026-07-16-ручная-выгрузка-отчётов.yaml
решение: Отчёты выгружаются вручную скриптом, без интеграции с планировщиком
причина: Нужно к сезону 1 сентября; интеграция — 2 недели, которых нет
проценты: 2 часа работы дежурного в неделю + риск пропустить выгрузку
владелец: Пётр (он же дежурит по отчётам)
триггер_возврата: Более 20 выгрузок в месяц ИЛИ первый пропуск ИЛИ 1 декабря
если_не_вернуть: Ручной шаг закрепится, дежурство подорожает, знание останется у одного человека

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

Что не надо чинить

Список отказов — такая же часть вашей работы, как список планов, и экономит он больше.

  • Код, который будет удалён, и редкие места с терпимыми последствиями: оставить как есть — решение, а не лень.
  • Красиво, но без процентов: устаревший стиль, «неправильные» имена, старая, но рабочая библиотека без уязвимостей. Апгрейд ради апгрейда — тоже долг, оплаченный вперёд. (Зависимость с эксплуатируемой уязвимостью — не долг вовсе, а инцидент со своим сроком.)
  • Долг чужой команды: соблазн починить самим велик, цена — поддержка навсегда. Сначала разговор о границах (глава 13).
  • То, что чинится процессом, а не кодом: ручная регрессия перед релизом иногда не про тесты, а про то, что релизы редкие и огромные.

Работа с командой вокруг долга

  • Кто называет долг, тот описывает проценты. Фильтр: половина «ужасного кода» отваливается на «покажи три случая», вторая половина становится сильным аргументом.
  • Сеньор, который хочет переписать на любимом стеке. Не спорьте о технологии — спросите про проценты, срок жизни модуля и что мы выключим. Нет ответов — это желание, а не решение; отказывать надо прямо и с причиной, иначе получите тихий саботаж.
  • Не превращайте долг в наказание («раз ты это написал, ты и чинишь» гарантирует, что в следующий раз проблему принесут не вам) и ротируйте владельцев: один и тот же человек на уборке — выгорание плюс монополия на знание, см. делегирование.
  • Показывайте результат. Погашенный долг невидим: ничего не случилось. Раз в квартал — сводка «что стало дешевле», иначе команда решит, что эта работа не считается.

Пределы вашей власти

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

Мини-итог

Чек-лист квартала одной строкой: реестр пересмотрен вместе с планом продукта; у каждого живого пункта есть проценты, владелец и критерий «готово»; в план попали 2–4 именованных пункта, а не процент; по крупным известно, сколько ещё живёт модуль; есть хотя бы один пункт, который вы решили не чинить; замерены проценты по погашенному в прошлом квартале. Главное из главы:

  • Долг — разрыв между устройством системы и тем, что от неё потребуется по плану, выраженный в стоимости будущих изменений. Без планов долга нет — есть некрасивый код.
  • Данных мало: лучшее измеренное — «Code Red» (наблюдательное), а самый полезный результат Google отрицательный: метрики плохо предсказывают то, что инженеры считают долгом.
  • Долг — портфель рисков: часть чинят, часть страхуют, часть принимают осознанно, часть удаляют вместе с функциональностью. Окупаемость зависит от оставшегося срока жизни кода, поэтому первый вопрос — к продукту, а не к команде.
  • Квота в процентах без владельца и именованных пунктов превращается в ноль за квартал, и никто не замечает, потому что сумму не считают.
  • Аргумент работает, когда переведён в единицы плана — задачи, дни, риски — с вариантами и обещанием замерить результат. Ультиматум работает один раз.
  • Осознанный долг — нормально, молчаливый — нет: проценты, владелец, триггер, последствия. Ваша власть ограничена: вы отвечаете за то, чтобы выбор был явным, а не за то, чтобы выиграть его.

Источники

Что дальше

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

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов