Контрактна сумісність 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 зможуть швидко еволюціонувати, а інтеграції - залишатися стабільними.