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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

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

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

Постановка проблемы

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

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

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

Подробное решение

Сначала зафиксируйте контракт и разделите изменения на совместимые и несовместимые. Совместимыми обычно считаются добавление необязательного запроса, добавление поля ответа при устойчивом к расширению чтении и добавление нового значения только при условии, что клиенты корректно обрабатывают неизвестные значения.

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

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

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

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

Ситуация из практики

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

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

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

Что кандидаты часто упускают

  1. Достаточно ли добавить новое необязательное поле, чтобы изменение считалось обратно совместимым?

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

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

  1. Почему нельзя просто выпускать новую версию при каждом изменении API?

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

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

  1. Что делать, если клиент неправильно обрабатывает неизвестные значения перечисления?

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

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