Контрактная совместимость API
Зачем нужна контрактная совместимость
Контрактная совместимость — это способность API эволюционировать без поломки существующих интеграций. В растущих системах API меняются чаще кода клиентов; совместимость позволяет выпускать фичи итеративно, не устраивая «больших переездов».
Ключевая идея: контракт — первичен, изменения проходят по правилам совместимости и проверяются автоматически.
Базовые понятия
Контракт — формальная спецификация интерфейса: ресурсы/методы/события, схемы данных, коды ошибок, лимиты, SLA, требования безопасности.
Поставщик (provider) — владелец API. Потребитель (consumer) — клиент/интеграция.
- Backward: новый поставщик работает со старыми потребителями.
- Forward: старый поставщик работает с новыми потребителями (обычно достигается «терпимыми читателями»).
- Full: соблюдены и backward, и forward (самый сильный вариант).
- Аддитивность — добавление необязательных элементов без ломки существующих.
Политика версионирования
Semantic Versioning (рекомендуется):- MAJOR — ломающие изменения (только при выпуске новой линии API: `/v2`, `service.v2`).
- MINOR — аддитивные изменения (новые необязательные поля/методы).
- PATCH — исправления без изменения контракта.
- Deprecation Policy: объявление устаревших элементов, окно поддержки (sunset), предупреждения в заголовках/метаданных, план снятия.
Безопасные vs опасные изменения
Безопасные (обычно backward-compatible)
Добавление необязательного поля в JSON/Protobuf/Avro.
Добавление нового эндпоинта/метода/события.
Расширение enum новыми значениями, если потребители толерантны к неизвестным значениям.
Повышение лимитов (например, `maxItems`) без ужесточения минимальных.
Добавление nullable с корректными дефолтами.
Изменение текста описаний/примеров.
Опасные (ломают совместимость)
Переименование/удаление полей, изменение их типа или обязательности.
Смена семантики статус-кодов/ошибок (например, было `200`, стало `204` или `404`).
Изменение формата идентификаторов (UUID → int).
Ужесточение валидации (строже минимумы/паттерны) без версии.
Изменение порядка и структуры в gRPC-стримах/событиях.
Переиспользование номеров тегов в Protobuf для новых полей.
Совместимость по стилям взаимодействия
REST/HTTP + JSON Schema
Аддитивность: новые поля помечаем как `optional`/`nullable`.
Tolerant Reader у клиента: игнорировать неизвестные поля; не полагаться на порядок.
Версионирование: мажор — в пути (`/v2`) либо в медиатипе (`application/vnd.example.v2+json`).
ETag/If-Match: для безопасных апдейтов без гонок.
Ошибки: единый формат (`type`, `code`, `title`, `detail`, `trace_id`), не меняйте значения `code` без мажора.
Пагинация: курсоры предпочтительнее offset; добавляйте поля `next_cursor`, не меняйте смысл существующих.
gRPC / Protobuf
Нумерация тегов неизменна. Удаленные теги не переиспользовать.
Новые поля — `optional`/`repeated` с разумными дефолтами на сервере.
Не меняйте порядок и обязательность сообщений в streaming-RPC.
Статусы ошибок — стабильные (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, и т. п.); новая семантика → новая версия метода/сервиса.
Event-driven (Kafka/NATS/Pulsar) + Avro/JSON Schema
Именование событий: `domain.action.v{major}`.
Новые поля — опциональные; выделяйте ядро и обогащения (`.enriched`).
Регистры схем: правила совместимости (BACKWARD/FORWARD/FULL) на тему/событие.
Расширение enum допустимо при tolerant reader на стороне потребителей.
Смена ключа партиционирования/порядка для агрегата = ломающие изменения.
GraphQL
Добавление полей/типов — безопасно; удаление/переименование — только через @deprecated и окно миграции.
Не меняйте типы/не nullable без мажора.
Контролируйте complexity/depth — лимиты являются частью контракта.
Паттерны устойчивой эволюции
Additive-first: расширяйте, не ломая.
Capability negotiation: клиенты сообщают, что поддерживают (заголовки/параметры/договоренности), сервер подстраивается.
Границы контракта: фиксируйте MGC (минимальный гарантийный контракт) и отделяйте расширения (модель обратной пирамиды).
Tolerance by default: клиенты игнорируют лишнее и корректно обрабатывают неизвестные значения enum (fallback).
Dual-write / Dual-emit: при мажорных изменениях некоторое время выпускайте `v1` и `v2` параллельно.
Sunset headers/Events: заранее уведомляйте о снятии версий.
Governance и автоматизация
API-линтеры:- OpenAPI/Spectral: именование, пагинация, коды ошибок, форматы полей.
- Buf/Protobuf: запрет переиспользования тегов, нотации пакетов.
- AsyncAPI/Schema Registry: совместимость схем на уровне CI.
- Каталог контрактов (SSOT): централизованный реестр схем/версий с историей диффов.
- API Guild: гильдия/комитет, принимающий правила, шаблоны и review изменений.
- Change Management: RFC/ADR, release notes, миграционные гайды.
Тестирование совместимости
Schema-diff в CI: блокируем ломающие изменения спек (OpenAPI-diff, Buf breaking, SR compatibility).
Consumer-Driven Contracts (CDC): Pact/похожие — проверка поставщика против контрактов конкретных потребителей.
Golden samples: эталонные запросы/ответы и события для регресса.
E2E Canary: раскатка на долю трафика/отдельные консюмер-группы.
Chaos/latency: проверка таймаутов/ретраев — изменение latency-SLO считается изменением контракта.
Миграции и депрекейт
1. Объявите депрекейт: пометьте элемент, укажите срок sunset и альтернативу.
2. Поддерживайте период совместимости: dual-write/dual-emit, мосты, адаптеры.
3. Соберите телеметрию: кто еще использует старое?
4. Коммуникация: рассылки, релиз-ноты, тестовые стенды.
5. Снятие: по истечении окна — удаление с фиксированным релизом.
Примеры изменений
REST
Было:json
{ "id":"p1", "status":"authorized" }
Стало (аддитивно, безопасно):
json
{ "id":"p1", "status":"authorized", "risk_score": 0. 12 }
Клиенты, игнорирующие неизвестные поля, не ломаются.
Protobuf
proto message Payment {
string id = 1;
string status = 2; // don't change tag numbers optional double risk_score = 3; // additive
}
Event
`payment.authorized.v1` (ядро) + `payment.enriched.v1` (обогащение). Потребители критического пути читают ядро и не зависят от обогащений.
Антипаттерны
Swagger-wash: спецификация формально есть, но поведение сервиса расходится с ней.
Breaking by stealth: поменяли тип/статус/формат без новой версии и окна миграции.
Сырые CDC-события как публичный контракт: утечка схем БД, невозможность эволюции.
Жесткий клиент: падает при неизвестных полях/значениях; отсутствие tolerant reader.
Переиспользование protobuf-тегов: тихая коррупция данных.
Латентность как «неконтрактная»: неожиданно удлинили p95 — потребители ломаются по таймаутам.
Чек-лист совместимости (перед мерджем)
- Изменения аддитивны (или мажорная версия подготовлена).
- Линтеры/дифф-чеки пройдены, правила совместимости — зеленые.
- Ошибки/коды/статусы не меняли семантику.
- Enum расширены без запрета старых значений; клиенты — tolerant.
- Границы MGC неизменны.
- Обновлены примеры/документация/SDK.
- Для мажора — план dual-write/dual-emit, sunset-дата, комм-план.
- Тесты CDC/Golden/E2E пройдены.
FAQ
Чем backward отличается от forward совместимости?
Backward — новые серверы не ломают старых клиентов. Forward — новые клиенты не ломаются на старых серверах (через tolerant reader и аккуратные дефолты).
Когда все-таки делать `/v2`?
Когда меняются инварианты/семантика, удаляются поля/методы, требуется новая модель безопасности — проще и честнее запустить новую линию.
Можно ли жить без Schema Registry/линтеров?
Теоретически — да, практически — это частые регрессы и «скрытые» ломки. Автоматизация окупается.
Enum можно расширять?
Да, если клиенты корректно обрабатывают неизвестные значения (fallback/ignore). Иначе — мажор.
Итог
Контрактная совместимость — это правила + дисциплина + автоматизация. Проектируйте аддитивно, версионируйте ломающие изменения, применяйте tolerant reader, автоматом проверяйте диффы и CDC, планируйте депрекейт. Так API смогут быстро эволюционировать, а интеграции — оставаться стабильными.