Прямая совместимость
Что такое прямая совместимость
Прямая совместимость (forward compatibility) — это способность системы корректно работать с более новыми клиентами или данными, чем те, под которые она изначально проектировалась. Проще: старый сервер не ломается, когда к нему приходит новый клиент; старый потребитель не падает, когда встречает новое сообщение.
От обратной совместимости (когда новая система поддерживает старых клиентов) forward отличается направлением ответственности: мы проектируем протоколы и клиенты так, чтобы «пережить» будущие расширения без тотального апгрейда всей экосистемы.
Базовые принципы
1. Tolerant reader & tolerant writer
Reader игнорирует неизвестные поля/заголовки и допускает новые enum-значения с корректным fallback.
Writer не отправляет ничего, что сервер явно не объявил как поддерживаемое (capabilities).
2. Capability negotiation
Явный обмен возможностями (фичи/версии/медиа-типы) на handshake-этапе. Клиент адаптирует свое поведение к ответу сервера.
3. Дефолтная деградация
Новые возможности считаются опциональными: если сервер/консьюмер их не поддерживает, сценарий все равно завершится полезным минимумом (MGC).
4. Стабильное ядро (MGC)
Минимальный гарантийный контракт неизменен; новшества живут как расширения.
5. Контракты ошибок как часть протокола
Предсказуемые коды/причины («фича не поддерживается», «медиа-тип неизвестен») позволяют клиенту автоматически откатиться на поддерживаемый режим.
6. Версии без сюрпризов
Мажорные линии отделены; минорные расширения не требуют обновления сервера/консьюмера.
Где это особенно важно
Публичные API с долгоживущими интеграциями (партнеры, SDK в мобильных приложениях).
Событийные платформы с множеством независимых консьюмеров.
Мобильные клиенты, которые обновляются медленнее, чем бэкенд.
Эдж/IoT, где часть парка устройств редко прошивается.
Паттерны реализации по стилям
REST/HTTP
Negotiation:- `Accept`/медиатипы с параметрами (`application/vnd.example.order+json;v=1;profile=risk`).
- `Prefer: include=...` для опциональных блоков.
- Заголовок `X-Capabilities: risk_score,item_details_v2`.
- Отправляет запрос в базовом формате, расширения — только если сервер подтвердил capability (через OPTIONS/desc/лид-эндпоинт).
- При `415/406/501` автоматически откатывается на поддерживаемый формат/метод.
- Ответ сервера: неизвестные параметры — игнорировать; лишние поля — допускаются; стабилен формат ошибок (`type/code/detail/trace_id`).
gRPC / Protobuf
Стабильные сервисы: новые методы/поля — аддитивно; старый сервер спокойно игнорирует неизвестные поля запроса.
Feature discovery: метод `GetCapabilities()` возвращает списки фич/лимитов. Клиент не вызывает «v2-метод», если сервер его не объявил.
Стриминг: фиксируйте порядок минимального набора сообщений; новые «фреймы» помечайте расширениями/типами, которые старый клиент игнорирует.
GraphQL
Forward-friendly: новые поля/типы появляются на сервере — старые клиенты их просто не запрашивают.
Догадки запрещены: клиент должен держать схему (интроспекция/кодоген) и не отправлять неизвестные директивы/переменные.
Деградация: если сервер не знает кастомной директивы/feature — клиент строит запрос без нее.
Event-driven (Kafka/NATS/Pulsar, Avro/JSON/Proto)
FORWARD-совместимость схемы в реестре: старые консьюмеры могут читать сообщения, записанные новой схемой.
Аддитивные поля с дефолтами: новые продюсеры не ломают старых консьюмеров.
Core vs Enriched: ядро остается прежним, новые сведения публикуются в `.enriched` или как опциональные поля.
Практики проектирования
1. Договор на минимальный запрос (MGC)
Операция должна иметь «узкую шейку», которую поддержат все серверы много лет.
2. Фича-флаги на уровне контракта
Опишите фичи как именованные возможности: `risk_score`, `pricing_v2`, `strong_idempotency`. Клиент включает их явно.
3. Явные коды ошибок для «не поддерживается»
HTTP: `501 Not Implemented`, `415 Unsupported Media Type`, детальные `problem+json`.
gRPC: `UNIMPLEMENTED`/`FAILED_PRECONDITION`.
Events: маршрут в DLQ с `reason=unsupported_feature`.
4. Не полагаться на порядок/полные списки
Клиент должен быть готов к новым значениям enum, отсутствию новых полей и к «дополнительным» свойствам.
5. Стабильные идентификаторы и форматы
Не меняйте формат ID/ключей партиционирования в рамках линии — это ломает forward на стороне читателей.
6. Документация «машиночитаемая»
Хостите дескрипторы: OpenAPI/AsyncAPI/Proto descriptors/GraphQL SDL. Клиенты могут сверить поддержку фич.
Тестирование forward-совместимости
Schema-diff в режиме FORWARD/FULL: новая схема валидирует старого потребителя/сервер.
Контрактные тесты клиента: новый клиент исполняется против старого сервера со включенными/выключенными фичами.
Golden requests: набор «новых» запросов прогоняется по «старому» серверу; ожидается деградация без критических ошибок.
Chaos/latency: проверка таймаутов/ретраев — новый клиент должен корректно пережить худшие SLA старого сервера.
Canary: часть новых клиентов работает с предыдущей серверной версией — собираем телеметрию ошибок/деградаций.
Наблюдаемость и операционные метрики
Доля запросов/сообщений с неподдержанными фичами и их автоматическими откатами.
Распределение по версиям клиентов (User-Agent/метаданные/claims).
Ошибки `UNIMPLEMENTED/501/415` и маршруты в DLQ с `unsupported_feature`.
Время деградации: p95/p99 для MGC против «расширенного» ответа.
Режимы совместимости в реестре схем
FORWARD: новая запись совместима со старым читателем (нужны дефолты, опциональность).
FULL: и FORWARD, и BACKWARD; удобно для публичных контрактов.
Рекомендация: для событий — BACKWARD у продюсера и FORWARD у консьюмера (через tolerant reader), для внешних API — FULL.
Примеры
REST (capabilities + деградация)
1. Клиент делает `GET /meta/capabilities` → `{ "risk_score": false, "price_v2": true }`.
2. На `POST /orders` отправляет базовые поля; `risk_score` не запрашивает, потому что сервер его не умеет.
3. Если случайно отправил `Prefer: include=risk_score`, сервер отвечает 200 без поля `risk_score` (или `Preference-Applied: none`) — клиент не падает.
gRPC (discovery)
`GetCapabilities()` вернул список методов/фич. Клиент не вызывает `CaptureV2`, если его нет — вместо этого использует `Capture` и локально преобразует входные данные до поддерживаемого вида.
Events (FORWARD в реестре)
Продюсер добавил поле `risk_score` (nullable с дефолтом). Старый консьюмер его игнорирует; его логика использует только стабильные поля ядра.
Антипаттерны
Жесткий клиент: фильтрует ответ по whitelist-полям и падает на незнакомом свойстве.
Неявные фичи: клиент начинает отправлять новый параметр без проверки capabilities.
Смена форматов ID/ключей в пределах линии → старые серверы/консьюмеры перестают понимать новые запросы/сообщения.
Зашитые предположения о полном списке enum (switch без default).
Логирование как контроль потока: парсинг строк ошибок вместо контрактных кодов.
Чек-лист внедрения
- Определен MGC; новые возможности помечены как опциональные.
- Описан и реализован capability negotiation (эндпоинт/метаданные/handshake).
- Клиенты игнорируют незнакомые поля и корректно обрабатывают новые enum (fallback).
- Контракты ошибок фиксируют «не поддерживается» предсказуемо (HTTP/gRPC/Event).
- Реестр схем настроен на FORWARD/FULL для соответствующих артефактов.
- Автотесты: schema-diff (FORWARD), контрактные тесты клиента против старого сервера, canary.
- Метрики: версия клиента, отказ фич, доля деградаций, p95 MGC.
- Документация/SDK публикуют список фич и примеры деградации.
FAQ
Чем forward отличается от backward на практике?
Backward: новый сервер не ломает старых клиентов. Forward: старый сервер не ломается от новых клиентов (или старый консьюмер — от новых сообщений). В идеале вы достигаете full.
Нужно ли всегда вводить capabilities?
Если ожидаете активную эволюцию без синхронных релизов — да. Это дешевле, чем держать десятки major-линий.
Как быть с безопасностью?
Новые фичи должны требовать отдельные scopes/claims. Если сервер их не поддерживает — клиент не должен снижать безопасность, а должен отказаться от фичи.
Можно ли «угадывать» поддержку по версии сервера?
Нежелательно. Лучше спрашивать явно (capabilities) или смотреть на медиатип/схему.
Итог
Прямая совместимость — это дисциплина договариваться о возможностях и безопасно деградировать. Стабильное ядро, capability negotiation, аддитивные расширения и предсказуемые ошибки позволяют новым клиентам и данным уживаться со старыми серверами и потребителями — без массовых релизов и ночных миграций.