Контракты и взаимодействие между компонентами#

Содержимое главы#

  • Что такое контракт в распределённой системе
  • Типы контрактов:
    • HTTP / RPC синхронные контракты
    • Асинхронные контракты (события)
  • Контракты как точка стабильности системы
  • Генерация контрактов и инструменты, примеры
  • Контракты событий и их особенности
  • Версионирование контрактов
    • Backward compatibility (обратная совместимость)
    • Forward compatibility (расширение моделей данных без поломки клиентов)
    • Разбор опасных и безопасных изменений
  • Типичные ошибки при проектировании контрактов
  • Практика

Что такое контракт#

В распределённых системах контракт - это не просто описание API или набор DTO. Контракт - это явное соглашение между независимыми компонентами, которые развиваются отдельно и могут находиться под управлением разных команд.

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

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

contract_meme

Виды контрактов и способы взаимодействия#

На практике чаще всего встречаются два класса контрактов: синхронные и асинхронные.

Синхронные контракты используются там, где нужен немедленный ответ: HTTP API, RPC-вызовы, gRPC. Они просты для понимания, но создают жёсткую связанность между компонентами. Отказ или замедление одного сервиса напрямую влияет на его зависимости.

Асинхронные контракты основаны на событиях. Сервис публикует факт произошедшего изменения, а потребители обрабатывают его независимо. Такой подход снижает связанность и повышает устойчивость системы, но усложняет отладку и работу с согласованностью данных. Появляется eventual-consistency для клиентов и потребителей.

Выбор между синхронным и асинхронным взаимодействием - это архитектурное решение, а не вопрос удобства разработки.

contract_types

Контракты событий#

Событийный контракт описывает бизнес-факт, а не команду или техническое действие. Например, OrderStatusChanged - это сообщение о том, что состояние заказа изменилось, а не инструкция, что с этим делать.

Ключевой момент - владение контрактом. Событие принадлежит домену-автору (publisher), в котором оно произошло. Этот домен определяет структуру события и правила его изменения. Остальные сервисы адаптируются под контракт, но не управляют им.

Неправильное проектирование событий часто приводит к избыточным или слишком абстрактным контрактам, которые со временем начинают протекать деталями внутренней реализации.

Контракты событий с данными#

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

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

{
  "order_id": 1,
  "changed_at": "dd/mm/yyyy",
  "changedData": {
    "status": "delivered"
  }
}

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

Контракты событий без данных#

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

Событие OrderStatusChanged требует уточнения дополнительных деталей для обработки.

{
  "order_id": 1,
  "changed_at": "dd/mm/yyyy"
}

Версионирование контрактов#

Любой публичный контракт со временем меняется. В распределённой системе важно не столько само изменение, сколько его совместимость с существующими потребителями. Не все изменения одинаково опасны.

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

  • Добавление нового необязательного поля в запрос
  • Добавление нового необязательного поля или структуры данных в ответ
  • Удаление входного параметра
  • Изменение входного параметра на опциональный ии ослабление валидации входных данных
  • Добавление нового значения в enum при условии, что клиенты корректно обрабатывают неизвестные значения

Список операций, которые скорее всего вызывают проблемы:

  • Изменение семантики существующего поля без изменения его имени
  • Изменение типа поля (например, int -> string)
  • Изменить значение в enum
  • Удалить значение из enum
  • Удалить обязательное поле из ответа
  • Переименование поля без поддержки старого
  • Изменение формата даты / времени
  • Добавление нового обязательного поля в запрос

Список потенциально опасных изменений

  • Использование enum без обработки unknown значений на стороне клиента
  • Удаление поля, которое “никто вроде бы не использует”
  • Замена null на отсутствие поля или наоборот (или замена null на 0)
  • Изменение дефолтных значений
  • Использование boolean вместо enum на растущих моделях

Ошибки на этом уровне редко проявляются сразу и часто обнаруживаются уже в продакшене.

При обратно-несовместимых изменениях, которые невозможно безопасно произвести

  • Выпускается новая версия обработчиков или событий например /v2/
  • /v1/ deprecated, подключение новых клиентов запрещено
  • Миграция клиентов на /v2/
  • Удаление первой версии

api_change

Фактически на любом проекте#

Системы используют аддитивный подход: новые поля добавляются, старые долго не удаляются, значения по умолчанию выбираются осознанно. Удаление и переименование считаются самыми опасными операциями и требуют отдельного планирования. Есть несколько версий эндпоинтов, потому что всех клиентов не успели мигрировать. Чем больше версий у контрактов, тем сложнее систему поддерживать.

Генерация контрактов#

Кодогенерация позволяет сделать контракт первичным артефактом системы. Из одного описания генерируются клиенты, серверные интерфейсы и модели данных.

Важно понимать ограничения этого подхода. Кодогенерация не решает архитектурных проблем и не защищает от плохого дизайна. Напротив, она делает ошибки более заметными и масштабируемыми.

OpenAPI (Swagger)#

Самый распространённый стандарт для описания HTTP API. Спецификация

  • Описание REST API в YAML / JSON
  • Генерация клиентов и серверных заглушек
  • Документация “из коробки”

Инструменты кодогенерации

  • Openapi-generator
  • Swagger-codegen

Подходит

  • Синхронные HTTP API
  • Внешние и публичные контракты
  • Интеграции между командами

Пример “создание заказа”, попробовать можно тут

openapi: 3.0.3
info:
  title: Orders Service API
  description: Public API for creating orders
  version: 1.0.0

servers:
  - url: https://api.example.com

paths:

  /orders:
    post:
      summary: Create order
      description: Creates a new order in the system
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order successfully created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateOrderResponse'
        '400':
          description: Invalid request
        '409':
          description: Business conflict (e.g. invalid state)

components:
  schemas:

    CreateOrderRequest:
      type: object
      required:
        - userId
        - items
      properties:
        userId:
          type: string
          description: Identifier of the user who creates the order
        items:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/OrderItem'
        comment:
          type: string
          description: Optional comment for the order

    CreateOrderResponse:
      type: object
      required:
        - orderId
        - status
        - createdAt
      properties:
        orderId:
          type: string
        status:
          type: string
          enum: [CREATED]
        createdAt:
          type: string
          format: date-time

    OrderItem:
      type: object
      required:
        - productId
        - quantity
      properties:
        productId:
          type: string
        quantity:
          type: integer
          minimum: 1

gRPC / бинарные контракты#

Контракт описывается в .proto файлах. Спецификация

  • Строго типизированные контракты
  • Быстрая сериализация
  • Эволюция схем

Подходит для

  • Внутренних сервисы
  • Строгого контроля версионирования

Пример, тоже создание заказа

syntax = "proto3";

package orders.v1;

option go_package = "github.com/example/orders/gen/go/orders/v1";
option java_package = "com.example.orders.v1";
option java_multiple_files = true;

// =======================
// Service
// =======================

service OrdersService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
}

// =======================
// Requests / Responses
// =======================

message CreateOrderRequest {
  string user_id = 1;

  repeated OrderItem items = 2;

  // Optional field - safe for extension
  string comment = 3;
}

message CreateOrderResponse {
  string order_id = 1;

  OrderStatus status = 2;

  string created_at = 3; // ISO-8601 (RFC3339)
}

// =======================
// Domain models
// =======================

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_CREATED = 1;
}

AsyncAPI#

Аналог OpenAPI, но для событий и очередей. Спецификация

Описывает

  • Топики
  • События
  • Payload
  • Продюсеров и консьюмеров

Подходит для

  • Event-driven архитектур
  • Контракты между продюсерами и консьюмерами

Пример события OrderCreated попробовать можно тут

asyncapi: 3.0.0

info:
  title: Orders Events API
  version: 1.0.0
  description: Domain events published by Orders service

channels:
  order.created:
    address: order.created
    messages:
      OrderCreated:
        $ref: '#/components/messages/OrderCreated'

operations:
  publishOrderCreated:
    action: send
    channel:
      $ref: '#/channels/order.created'
    messages:
      - $ref: '#/channels/order.created/messages/OrderCreated'

components:
  messages:
    OrderCreated:
      name: OrderCreated
      title: Order created event
      contentType: application/json
      payload:
        $ref: '#/components/schemas/OrderCreatedPayload'

  schemas:
    OrderCreatedPayload:
      type: object
      required:
        - eventId
        - orderId
        - userId
        - status
        - createdAt
      properties:
        eventId:
          type: string
        orderId:
          type: string
        userId:
          type: string
        status:
          type: string
          enum: [CREATED]
        createdAt:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'

    OrderItem:
      type: object
      required:
        - productId
        - quantity
      properties:
        productId:
          type: string
        quantity:
          type: integer
          minimum: 1

Apache Avro / Schema Registry#

Часто используется вместе с Kafka. Спецификация

  • Контракты сообщений
  • Контроль совместимости (backward / forward)
  • Централизованное хранение схем

Подходит для

  • Data-платформ
  • Потоковой обработки
  • Event sourcing

Рекомендация для курса#

Для обучения и практики лучше всего:

  • OpenAPI - для HTTP контрактов
  • AsyncAPI - для событий
  • Protobuf - для строгих внутренних контрактов

Антипаттерны проектирования контрактов#

В распределённых системах часто встречаются следующие проблемы:

  • Утечки внутренних моделей через публичные контракты
  • Чрезмерная детализация API
  • Отсутствие владельца контракта
  • Попытка спрятать бизнес-логику в схеме
  • Breaking changes без стратегии миграции

Эти ошибки редко выглядят критичными на старте, но со временем становятся источником технического долга и нестабильности системы.

Дополнительные источники для изучения#

Контакты#

Поддержать автора