Зворотна сумісність
Що таке зворотна сумісність
Зворотна сумісність (backward compatibility) - властивість системи приймати і коректно обробляти старих клієнтів/споживачів, коли система оновлюється. Простіше: ви випускаєте нову версію сервісу/подій, а вже існуючі інтеграції продовжують працювати без змін.
Ключ: не ламати домовленості. Будь-яка еволюція - через додавання, а не переробку вже випущеного.
Базові принципи
1. Additive-first
Нові поля/методи/події додаються опціонально. Нічого існуючого не видаляється і не змінює сенс.
2. Мінімальний гарантійний контракт (MGC)
Визначте ядро - набір полів/операцій, без яких сценарій втрачає сенс. Ядро стабільне. Все інше - розширення.
3. Tolerant reader
Клієнти ігнорують невідомі поля і коректно обробляють нові значення enum (fallback).
4. Політика версій
Ламаючі зміни - тільки через major-лінію ('/v2','payments. v2`, `event. v2`). Мінорні - адитивні.
5. Спостережуваність - частина контракту
У логах/трейсах і метриках видно версію клієнта, формат, capability-прапори. Це дозволяє управляти міграцією.
Безпечні vs небезпечні зміни
Зазвичай безпечні (BC-OK)
Додавання необов'язкових полів (JSON/Avro/Protobuf'optional '/' nullable').
Додавання нових ендпоінтів/методів/подій.
Розширення enum додатковими значеннями (при tolerant reader).
Ослаблення валідації (збільшення максимумів, додавання альтернативних форматів).
Додавання заголовків/метаданих, що не впливають на зміст.
Небезпечні (Breaking)
Видалення/перейменування полів, зміна типу або обов'язковості існуючих полів.
Зміна семантики статусів/кодів помилок.
Перевикористання protobuf-тегів під інші поля.
Зміна ключа партіонування події (ламає порядок для агрегату).
Посилення SLA/таймаутів, через що старі клієнти починають падати.
За стилями взаємодії
REST/HTTP + JSON
Адитивність: нові поля - «optional», сервер не вимагає їх від старих клієнтів.
Версії: major - в дорозі ('/v2') або медіатіпе; minor - через розширення і'? include = '/'? fields ='.
Помилки: єдиний формат; не змінювати коди/семантику без major.
ETag/If-Match: для безпечних апдейтів без гонок.
Ідемпотентність: 'Idempotency-Key'для POST - старі клієнти не «подвоюють» ефект при ретраях.
gRPC / Protobuf
Теги незмінні. Видалені теги не перевикористати.
Нові поля - «optional »/« repeated»; значення за замовчуванням коректно обробляються старим кодом.
Стрімінг: не змінювати порядок/обов'язковість повідомлень в рамках minor.
Помилки - стабільний набір статусів; нова семантика → новий метод/сервіс ('.v2').
Event-driven (Kafka/NATS/Pulsar) + Avro/JSON/Proto
Іменування: `domain. action. v{major}`.
Core vs Enriched: ядро стабільне; збагачення - окремі типи/теми ('.enriched').
Режим сумісності схем: частіше BACKWARD; CI блокує несумісні зміни.
Партіонування: ключ (наприклад,'payment _ id') - частина контракту; міняти його - breaking.
GraphQL
Додавання полів/типів - ОК; видалення/перейменування - через «@deprecated» і вікно міграції.
Не підвищуйте «nullable → non-nullable» без major.
Контролюйте complexity/depth - зміна лімітів = зміна контракту.
Патерни, що допомагають зберегти BC
Модель зворотної піраміди: стабілізуйте ядро, розширюйте опціонально.
Capability negotiation: клієнт повідомляє підтримувані можливості ('X-Capabilities '/handshake), сервер підлаштовується.
Dual-run / dual-emit: на час міграції тримайте одночасно «v1» і «v2».
Адаптери: проксі/гейтвей переводять запити'v1↔v2'для «важких» клієнтів.
Expand-and-contract (для БД): спочатку додайте нове, почніть писати/читати, тільки потім видаляйте старе.
Governance і процес
1. Каталог контрактів (реєстр схем): єдине джерело істини з політиками сумісності.
2. Лінтери і дифф-чеки в CI/CD: OpenAPI-diff, Buf-breaking, перевірка сумісності Avro/JSON Schema.
3. CDC/Consumer-Driven Contracts: провайдер перевіряється на реальні контракти споживачів.
4. Golden samples: еталонні запити/відповіді/події для регресу.
5. Change management: RFC/ADR на breaking, плани sunset, комунікація.
Депрекейт і зняття старих версій
Позначте застаріле («@deprecated», описи, заголовки «Deprecation», «Sunset»).
Вікно міграції: заздалегідь оголошена дата, тестовий стенд, приклади коду.
Телеметрія використання: хто ще на'v1'? сегментуйте метрики/логи за версією.
Dual-run до нуля трафіку, потім - видалення.
Спостережуваність та операційні метрики
Відсоток запитів/повідомлень за версіями.
Частка помилок/таймаутів у старих клієнтів після релізу.
Частка несумісних payload (валідація схемою на шлюзі/стрім-фільтрах).
Лаг міграції споживачів (скільки ще слухають'v1').
Тестування зворотної сумісності
Schema-diff: fail при remove/rename/type-change.
Контракт-тести: старі SDK/клієнти ганяються проти нової реалізації.
E2E-канарка: частина старого трафіку на нову версію, порівняння p95/p99, кодів, ретраїв.
Реплей подій: проекції збираються новою логікою зі старого лога без розбіжностей.
Fault-injection: затримки/часткові відповіді - старі клієнти не падають.
Приклади
REST (адитивно)
Було:json
{ "id": "p1", "status": "authorized" }
Стало:
json
{ "id": "p1", "status": "authorized", "risk_score": 0. 12 }
Старі клієнти, ігноруючи'risk _ score', продовжують працювати.
Protobuf (теги)
proto message Payment {
string id = 1;
string status = 2;
optional double risk_score = 3 ;//new field, safe
}
//Tags 1 and 2 cannot be changed/deleted without v2
Події (ядро + збагачення)
`payment. authorized. v1'- ядро (мінімум фактів).
`payment. enriched. v1'- деталі; споживачі ядра не залежать від збагачень.
Антипатерни
Swagger-wash: оновили схему, але сервіс поводиться по-старому (або навпаки).
Приховані ломки: змінили сенс поля/статусу без версії.
Перевикористання protobuf-тегів: «тиха» корупція даних.
Жорсткі клієнти: падають на незнайомих полях/enum; немає tolerant reader.
Mega-endpoint: одне-в-одному - будь-яка зміна стає потенційною ломкою.
Чек-лист перед релізом
- Зміни адитивні; ядро (MGC) не зворушено.
- Лінтери/диф-чеки пройдені; breaking-прапорів немає.
- Клієнтські SDK оновлені (або не потрібні для адитивного розширення).
- Включений tolerant reader у клієнтів; enum-fallback перевірений.
- Метрики/логи містять версію і capability-прапори.
- Для потенційної ломки є '/v2', dual-run і план sunset.
- Документація/приклади оновлені, є golden-набори.
FAQ
Backward vs forward - в чому різниця?
Backward - нові сервери працюють зі старими клієнтами. Forward - нові клієнти коректно працюють зі старими серверами (за рахунок tolerant reader і акуратних дефолтів). Повне коло - full compatibility.
Чи потрібно завжди робити '/v2'для великих змін?
Так, якщо ламаються інваріанти/типи/ключі/семантика. Інакше - зберігайте лінію і еволюціонуйте адитивно.
Як бути з enum?
Додавайте нові значення, не змінюючи сенс старих. Клієнти повинні мати fallback при невідомому значенні.
Що робити, якщо вже «зламали»?
Відкат, hot-fix адаптер, випуск'v2'з dual-run, комунікація і міграційний гайд.
Підсумок
Зворотна сумісність - це дисципліна еволюції: стабілізуйте ядро, розширюйте адитивно, впроваджуйте tolerant reader, автоматизуйте перевірки і ведіть усвідомлений депрекейт. Так ви зможете швидко розвивати платформу, не залишаючи клієнтів під уламками «непомітних» змін.