Короткий ответ
Нужен длительный период параллельной работы старой и новой версии за одним бэкендом с общей бизнес-логикой (новая версия — это, как правило, другой слой трансляции данных, а не полностью отдельный сервис), прозрачный мониторинг реального трафика по версиям, явная коммуникация и поэтапное отключение старой версии только на основе наблюдаемого снижения трафика, а не фиксированной даты — конкретная длительность периода и критерии отключения зависят от состава и договорных обязательств перед интеграторами.
Как это работает подробнее
параллельная поддержка двух версий не должна означать дублирование бизнес-логики — обычно практичнее иметь единый внутренний слой данных/логики и два "адаптера" представления (сериализации) для v1 и v2 поверх него, чтобы изменения в бизнес-правилах не нужно было вносить в двух местах. Миграция данных схемы (если затронута структура БД) должна быть спроектирована так, чтобы оба адаптера могли корректно работать с одной и той же актуальной моделью данных одновременно (expand-and-contract подход: сначала расширить схему данных совместимо, перевести всех потребителей, затем убрать старые поля/структуры).
Пример кода
# Единый внутренний слой данных + два адаптера представления для v1 и v2
def get_order_internal(order_id: int) -> OrderData:
return orders_repo.fetch(order_id) # единая бизнес-логика и модель данных
def serialize_order_v1(order: OrderData) -> dict:
return {"id": order.id, "full_name": f"{order.first_name} {order.last_name}", "total": order.total}
def serialize_order_v2(order: OrderData) -> dict:
return {"id": order.id, "first_name": order.first_name, "last_name": order.last_name, "total": order.total, "currency": order.currency}
@app.get("/v1/orders/{order_id}")
def get_order_v1(order_id: int):
return serialize_order_v1(get_order_internal(order_id))
@app.get("/v2/orders/{order_id}")
def get_order_v2(order_id: int):
return serialize_order_v2(get_order_internal(order_id))Что использовать на практике
- Рекомендация. нужно заранее инструментировать API метриками по версии и по конкретному клиенту (API-ключу), чтобы видеть, кто ещё использует старую версию, и иметь возможность целенаправленно связаться с оставшимися интеграторами перед финальным отключением, а не ориентироваться только на общий агрегированный трафик. Решение об отключении старой версии должно приниматься на основе данных (доля/абсолютное число активных ключей на v1 ниже порога и стабильно снижается), а не только по формально прошедшему сроку уведомления — принудительное отключение при значимом остаточном трафике создаёт риск массовых инцидентов у партнёров и репутационный риск для компании. Компромисс: длительный период параллельной поддержки с общим внутренним слоем логики и адаптерами представления снижает риск инцидентов у партнёров и даёт время на плавную миграцию, но увеличивает затраты на поддержку (два набора контрактных тестов, более сложная схема данных на переходный период, дольше живёт технический долг старой версии); агрессивное отключение по жёсткому сроку быстрее снижает этот технический долг и издержки поддержки, но переносит риск простоя и репутационного ущерба на внешних интеграторов, которые не успели мигрировать, и на саму компанию через поток инцидентов и обращений в поддержку. Критерий выбора — коммерческая значимость оставшихся интеграторов на старой версии, контрактные обязательства по срокам поддержки и готовность компании нести стоимость длительной параллельной поддержки против риска принудительного отключения.
Подводные камни
- Риски. реализовывать v1 и v2 как полностью независимые кодовые пути с дублированием бизнес-логики, из-за чего последующие изменения правил нужно синхронизировать вручную в двух местах и легко допустить рассинхронизацию поведения между версиями; устанавливать жёсткую дату отключения без мониторинга реального остаточного трафика и без возможности её сдвинуть при обнаружении значимого числа всё ещё активных интеграторов.