
Когда формат данных меняется непредсказуемо, ломаются интеграции, растут затраты на откаты и ручные правки. Стабильные контракты — это способ выпускать изменения быстро и безопасно:
Контракт — это спецификация формата данных и допустимых изменений во времени. Он одинаково важен для REST/JSON, gRPC/Protobuf и потоков событий (Kafka/Avro).
Для большинства продуктовых API и событий достаточно назад совместимости: мы можем добавлять поля, помечать как необязательные и заранее объявлять план вывода из эксплуатации (sunset) для старых полей.
Поддерживайте спецификацию как код, проверяйте в CI. Пример: добавим необязательное поле loyaltyLevel для клиента.
openapi: 3.0.3
info:
title: Customer API
version: 1.3.0
paths:
/customers/{id}:
get:
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Customer'
components:
schemas:
Customer:
type: object
required: [id, email]
properties:
id: { type: string }
email: { type: string, format: email }
name: { type: string, nullable: true }
loyaltyLevel: # новое необязательное поле
type: string
enum: [basic, silver, gold]
description: "Необязательное поле. Будет обязательным c 2026‑01‑01 — следите за объявлениями."
Проверяйте обратную совместимость спеки: линтеры (например, openapi-diff) детектируют опасные изменения — удаление полей, сужение enum, ужесточение ограничений.
В Protobuf совместимость держится на номерах полей и внимательном отношении к типам.
Версия v1:
syntax = "proto3";
package customer.v1;
message Customer {
string id = 1;
string email = 2;
string name = 3; // может отсутствовать на стороне сервера
}
Версия v2: поле name выведем из эксплуатации, добавим loyalty_level и отметим зарезервированный номер.
syntax = "proto3";
package customer.v2;
import "google/protobuf/wrappers.proto";
message Customer {
string id = 1;
string email = 2;
// 3 — резервируем, чтобы никто не переиспользовал старый номер
reserved 3;
// Новое необязательное поле с обёрткой — отличаем отсутствие от пустого
google.protobuf.StringValue loyalty_level = 4; // "basic" | "silver" | "gold"
}
Если нужен «мягкий» переход, можно временно оставить имя как deprecated:
message CustomerV1Compat {
string id = 1;
string email = 2;
string name = 3 [deprecated = true];
google.protobuf.StringValue loyalty_level = 4;
}
Avro-схемы хорошо подходят для событий и потоков: компактная сериализация и строгое определение совместимости через реестр схем.
Схема v1:
{
"type": "record",
"namespace": "events.customer",
"name": "CustomerCreated",
"fields": [
{"name": "id", "type": "string"},
{"name": "email", "type": "string"}
]
}
Добавим необязательное поле loyaltyLevel с умолчанием — это назад совместимо.
{
"type": "record",
"namespace": "events.customer",
"name": "CustomerCreated",
"fields": [
{"name": "id", "type": "string"},
{"name": "email", "type": "string"},
{"name": "loyaltyLevel", "type": ["null", {"type": "string", "default": "basic"}], "default": null}
]
}
Установим режим полной транзитивной совместимости для темы customers.created-value.
# Установить режим FULL_TRANSITIVE
curl -s -X PUT \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"compatibility": "FULL_TRANSITIVE"}' \
http://localhost:8081/config/customers.created-value
# Проверить совместимость новой схемы c последней версией
curl -s -X POST \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "{\"type\":\"record\",\"namespace\":\"events.customer\",\"name\":\"CustomerCreated\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"loyaltyLevel\",\"type\":[\"null\",{\"type\":\"string\",\"default\":\"basic\"}],\"default\":null}]}"}' \
http://localhost:8081/compatibility/subjects/customers.created-value/versions/latest
Если совместимость нарушена, реестр вернёт ошибку, а публикация схемы или запись сообщения будет отклонена в рантайме — это предотвращает инциденты.
Думайте о контрактах как о миграциях, только для данных «на проводе».
Шаги безболезненной эволюции:
Когда команд много, «договор на словах» не работает. Потребительские контракты позволяют потребителю описать свои ожидания, а провайдеру — автоматически проверить, что он им соответствует.
Небольшой пример на JavaScript с использованием Pact, который проверяет, что поле email остаётся обязательным, а loyaltyLevel — опциональным.
// package.json должен содержать pact и jest
// npm i --save-dev @pact-foundation/pact jest
const path = require('path');
const { Pact } = require('@pact-foundation/pact');
const { Matchers } = require('@pact-foundation/pact');
const provider = new Pact({
consumer: 'customer-web',
provider: 'customer-api',
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'warn'
});
describe('Customer API contract', () => {
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
test('GET /customers/123 returns mandatory email and optional loyaltyLevel', async () => {
await provider.addInteraction({
state: 'customer 123 exists',
uponReceiving: 'a request for customer 123',
withRequest: { method: 'GET', path: '/customers/123' },
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: Matchers.like('123'),
email: Matchers.like('user@example.com'), // обязательно
loyaltyLevel: Matchers.somethingLike('gold') // может отсутствовать
}
}
});
// В реальном тесте — вызов клиента и проверка ответа
await provider.verify();
});
});
В CI провайдер поднимает проверку контракта и не даёт сломать потребителей незаметно.
Пример шага CI для проверки Avro-схемы через Schema Registry:
#!/usr/bin/env bash
set -euo pipefail
SUBJECT="customers.created-value"
SCHEMA_FILE="schemas/customer_created_v2.avsc"
REGISTRY_URL="http://localhost:8081"
BODY=$(jq -n --arg schema "$(jq -c . < "$SCHEMA_FILE")" '{schema: $schema}')
curl -s -X POST \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data "$BODY" \
"$REGISTRY_URL/compatibility/subjects/$SUBJECT/versions/latest" \
| jq -e '.is_compatible == true' > /dev/null
echo "Schema is compatible"
Стабильные контракты — это дисциплина инженерных решений и процессы вокруг них. Правильные режимы совместимости, спецификации как код, Schema Registry, контрактные тесты и обязательные проверки в CI позволяют выпускать изменения без паники и ночных откатов. Бизнес от этого получает предсказуемость сроков, меньше инцидентов и ускорение интеграций с клиентами и партнёрами.
Бонус: однажды вы выстроите этот конвейер — и каждый следующий контракт будет эволюционировать заметно дешевле и спокойнее, чем предыдущий.