Публичный интерфейс прикладного программирования это обещание. Сервер обещает отвечать на конкретный запрос конкретной формой ответа, клиенты строят на этом обещании свой код, и вместе эти обещания живут годами. В четверг кто-то выпускает релиз, в котором поле amount переименовано в total, потому что так красивее, и к вечеру падают интеграции, о которых автор изменения не знал. Дальше у команды два пути. Первый: признать разрыв совместимости свершившимся фактом и чинить последствия по мере поступления. Второй: признать, что эволюция интерфейса это процесс с объявлениями, окнами перехода и метриками, и разрушительные изменения выпускать только после того, как старыми версиями перестали пользоваться. Второй путь старше, скучнее и дешевле. Разберём, из чего он состоит и почему резкий отказ от старой версии обходится дороже всего её поддержки.

Что ломается у клиентов при резком отказе от старой версии API

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

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

Деньги здесь считают просто. Восстановление удалённого поля занимает час работы. Разбор инцидента, письма, отчёты и объяснения занимают неделю чужого времени. Утраченное доверие не измеряется часами: команды, однажды пережившие разрыв без предупреждения, в следующий раз застрахуются. Они обложат каждый вызов дополнительными проверками, перестанут обновлять интеграцию и при первой возможности уйдут к поставщику, который обещания держит. Резкий разрыв выглядит экономией на поддержке старого кода, на деле это перекладывание расходов на клиентов, а клиенты такие расходы помнят.

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

Семантическое версионирование как язык договорённостей между системами

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

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

Заголовки Deprecation и Sunset как автоматическое объявление о будущем отключении

Интернет-стандарты давно описали способ сообщать об устаревании прямо в ответе сервера. Заголовок Deprecation закреплён документом RFC 9745, вышедшим в марте 2025 года в категории предложенных стандартов: он сообщает потребителю ресурса, что ресурс устарел или будет признан устаревшим. Заголовок Sunset закреплён документом RFC 8594 от мая 2019 года и называет момент, после которого ресурс, вероятно, перестанет отвечать. Вместе они превращают расплывчатое "мы что-то там deprecated" в машиночитаемую метку, которую ловят тесты, мониторинг и автоматические сборщики документации:

Deprecation: version="v1"
Sunset: Wed, 31 Dec 2025 23:59:59 GMT
Link: </migration-guide-v2>; rel="deprecation"

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

Дальше заголовки встраивают в конвейер проверки. Сборочная линия клиента заводит правило: тестовый прогон считает ошибкой каждый ответ с Deprecation, на который нет задачи в трекере. Так устаревший вызов не может тихо жить годами: он либо починен, либо числится в списке работ. Почтовые уведомления дублируют заголовки для тех, кто в собственные журналы не смотрит: письмо при первом обращении, напоминание в середине окна и последнее предупреждение за две недели до даты из Sunset. Три письма звучат навязчиво, но именно они превращают дату отключения из абстракции в календарную встречу.

Окно перехода и фазы эволюции API от объявления до отключения

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

  1. добавить новое поведение рядом со старым, не трогая существующие поля, маршруты и коды ответов;
  2. объявить старое поведение устаревшим: заголовки в ответах, предупреждения в журналах, письма интеграторам с датой отключения;
  3. снижать долю старого трафика, отслеживая обращения каждого клиента и напоминая молчунам;
  4. отключить старое поведение и удалить его код после истечения объявленного срока.

Образцовую формальную политику в открытом мире показал Kubernetes. Правила проекта различают уровни зрелости интерфейсов: стабильные версии могут быть объявлены устаревшими, но не удаляются в пределах мажорного релиза системы; бета-версии объявляются устаревшими не позднее девяти месяцев или трёх минорных релизов после появления, и столько же времени им даётся после объявления до прекращения обслуживания; альфа-версии могут исчезнуть в любом релизе без предупреждения. Есть в политике и правило прямого обмена: интерфейс не объявляется устаревшим в пользу менее стабильного, а данные обязаны читаться и писаться во всех поддерживаемых версиях без потерь.

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

Типовое окно для внешнего интерфейса средней величины занимает девяносто дней. День первый: новое поведение рядом со старым и заголовок устаревания в ответах. День тридцатый: письмо каждому интегратору с его собственными цифрами обращений. День шестидесятый: второе напоминание тем, чья доля старого трафика не упала. День девяностый: отключение вызовов, чья доля упала ниже одного процента, и личный разговор с оставшимися. Окно можно растягивать и сжимать, важно одно: даты называют до изменения, а не после.

Закон Хайрама о зависимости пользователей от любого наблюдаемого поведения

У эволюции интерфейсов есть неудобный закон, сформулированный инженером Хайрамом Райтом: если у системы достаточно пользователей, каждый элемент её наблюдаемого поведения найдёт того, кто на него полагается. Не только поля ответа, на которые есть документация. Порядок ключей в ответе. Формулировки сообщений об ошибках, которые кто-то разбирает строковыми методами. Время ответа, в которое кто-то успел встроить ожидание.

Закон Хайрама объясняет, почему "мы всего лишь поправили вывод" ломает интеграции чаще, чем честные изменения схемы. Отсюда два вывода. Первый: контракт фиксируют явно, включая формат ошибок и правила умолчаний, всё неописанное признаётся хрупким и не считается обещанием. Второй: обратная совместимость это про описанное поведение. Держать неописанные мелочи неизменными невозможно, значит, задача не в вечной заморозке, а в ясной границе между обещанным и случайным.

Совместимые приёмы изменения ответов без разрыва клиентских интеграций

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

{
  "order_id": 40211,
  "total": "1490.00",
  "currency": "RUB",
  "vat_amount": "248.33"
}

Поле vat_amount появилось позже остальных, и ни одна старая интеграция не заметила его появления. Безопасно добавляются и новые параметры запросов, если у каждого есть значение по умолчанию: старый вызов без параметров обязан работать как раньше. Опасно переопределять смысл существующих значений: параметр сортировки, который начал сортировать по второму ключу, ломает постраничный вывод у половины клиентов. Хочется изменить поведение, вводите новый параметр с новым именем, а старое значение оставьте синонимом до конца окна перехода. Обратный манёвр, переименование total в order_total с молчаливым изъятием старого, сломал бы всех сразу, поэтому его проводят окном перехода: сначала новое поле живёт рядом со старым, потом старое объявляется устаревшим с датой отключения, и только потом, когда метрики говорят, что старым полем пользуются единицы, за этими единицами звонят по телефону. Код удаляют последним, уже после тишины в мониторинге.

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

Политика поддержки версий и правила окончательного выхода из старой

Инструмент выбора версии описывают в договоре с клиентами заранее. Вариантов три, и у каждого свой характер. Версия в адресе, например /v2/orders, проста, видна в логах и кэшах, но плодит параллельные маршруты. Версия в заголовке запроса не шумит в адресах: так работает публичный интерфейс одного крупного портала разработки, где версия передаётся строкой вида 2022-11-28. Дата-ориентированные версии закрепляются за учётной записью клиента: так живут крупные платёжные платформы, где аккаунт привязан к версии вида 2024-06-15 и обновляется вручную, по готовности владельца.

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

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