intDataBlog
← ЖурналМаршруты обучения ↗

События меняются: как обновлять контракт без одновременного релиза всех сервисов

Совместимость схемы, смысл полей, старые сообщения и порядок обновления производителей и потребителей.

Событие сохранилось в очереди вчера, а обработчик обновился сегодня. Даже если все сервисы сейчас используют новую версию кода, при повторном чтении история вернёт старый формат. Совместимость нужна не только между соседними релизами, но и между кодом и уже записанными данными.

Schema Registry позволяет задавать правила проверки совместимости. В терминологии Confluent BACKWARD означает, что новый читатель способен прочитать данные предыдущей схемы; transitive-режимы проверяют более длинную историю версий. Направление совместимости влияет на порядок обновления клиентов. Confluent: schema evolution

Схема проверяет не весь смысл

Представим учебное событие reservation.confirmed. В первой версии поле start_at содержит UTC-время начала. Во второй разработчик оставил строковый тип и то же имя, но начал записывать местное время без часового пояса. Формальная структура почти не изменилась, а интерпретация сломалась.

Поэтому контракт описывает единицы измерения, временную зону, допустимые значения и смысл отсутствия поля. Для денежных значений важны валюта и представление суммы; для количества — что именно считается. Эти свойства лучше проверить на конкретных примерах, чем ограничиться формулировкой «поле string».

В учебном конверте можно явно отделить тип события, его версию и полезную нагрузку:

{
  "event_id": "event-demo-42",
  "type": "reservation.confirmed",
  "schema_version": 1,
  "data": {
    "reservation_id": "r-demo-17",
    "start_at": "2026-08-07T09:00:00Z"
  }
}

Это локальный пример, не объявленный стандарт события. Система должна заранее определить, откуда берётся ID и в каком пространстве он уникален. Номер версии полезен только вместе с существующим правилом чтения, а не как украшение JSON.

Добавление поля — тоже решение

Допустим, появляется необязательное поле location. Старый читатель может игнорировать его, если его формат и реализация это допускают. Новый должен знать, что делать со старым сообщением без location: показать «место уточняется», запросить отдельный источник или признать данных недостаточными.

Подставить произвольное значение по умолчанию опаснее, чем кажется. Если отсутствие страны заменить на конкретную страну, технически валидное сообщение породит неверный бизнес-результат. Политика отсутствующих данных является частью предметного решения.

Для конкретного формата — Avro, Protobuf или JSON Schema — допустимые изменения различаются. Настройка registry не освобождает от проверки фактического сериализатора и потребителя. Согласованная схема бесполезна, если один обработчик всё равно падает на незнакомом поле.

Изменение значения требует миграционного пути

Если прежнее событие означало завершённую операцию, нельзя незаметно переопределить его как намерение выполнить её позже. Потребители могли уже привязать к нему уведомление, списание ресурса или открытие доступа.

Для несовместимого смысла полезнее новый тип события или отдельная версия с понятным периодом сосуществования. Тогда можно поэтапно обновить обработчики и доказать, что старый поток больше не нужен. Решение о двойной публикации требует отдельного анализа дублей: два формата не должны запускать одно и то же действие дважды.

Проверять историю, а не только новый пример

Минимальный набор учебных сообщений включает старый формат, новый, неизвестное необязательное поле, отсутствующее значение и событие с неподдерживаемой версией. Для каждого задают ожидаемый результат: применить, пропустить по договорённости, отправить на разбор или остановить обработку.

Проверки запускают для старого и нового читателя. Отдельно воспроизводят сохранённую историю и повторную доставку. Если данные хранятся долго, совместимость с предпоследней схемой может оказаться недостаточной.

Хороший контракт позволяет разным командам выпускаться независимо, но эта независимость появляется из точного смысла и проверяемых переходов. Наличие поля version само по себе её не создаёт.