Микросервисы обмениваются сообщениями через брокер. Без чёткого контракта любое изменение в формате события ломает потребителей. Однажды переименование поля в событии OrderShipped привело к падению трёх сервисов в 3:00 ночи. Восстановление заняло 4 часа, а средний убыток от такого инцидента — $15 000. Event Schema — это контракт, который гарантирует, что все изменения явные и контролируемые. Он предотвращает 80% проблем совместимости ещё до деплоя. Свяжитесь с нами — мы поможем спроектировать схемы для вашей архитектуры и избавиться от ночных падений.
Avro с Schema Registry в 3 раза быстрее при сериализации, чем JSON без схемы, и обеспечивает строгую типизацию. Мы гарантируем обратную совместимость на всех этапах эволюции — так вы избежите ночных инцидентов и ускорите разработку.
Почему Avro, а не JSON?
Avro — бинарный формат сериализации, который компактнее JSON в 3-5 раз. Он строго типизирован: поле может быть только того типа, который описан в схеме. Schema Registry автоматически проверяет совместимость новой схемы со старыми: если вы добавили обязательное поле без default, регистр отклонит регистрацию. JSON не предоставляет таких гарантий, и ошибки совместимости обнаруживаются только в рантайме.
| Характеристика | Avro | JSON (без схемы) |
|---|---|---|
| Типизация | строгая | динамическая |
| Размер сообщения | компактный (бинарный) | избыточный (текстовый) |
| Совместимость | автоматическая (Schema Registry) | ручная |
| Скорость сериализации | высокая (до 3x быстрее) | низкая |
| Поддержка эволюции | встроенная (default, alias) | отсутствует |
Какие принципы лежат в основе Event Schema?
События описывают факты, не команды. OrderShipped — это факт. ShipOrder — это команда. Событие произошло и не может быть отменено (только компенсировано другим событием).
Схема должна быть самодостаточной. Консьюмер не должен делать дополнительных запросов для обработки события. Все нужные данные — в теле события.
Обратная совместимость по умолчанию. Старые консьюмеры должны работать с новыми событиями без изменений.
Структура события
{ "eventId": "01HQ2XK4VB8M9QXYZ123456789", "eventType": "order.shipped", "eventVersion": "1.2", "occurredAt": "2025-03-28T14:22:00.000Z", "producedBy": "order-service", "correlationId": "req-abc-123", "causationId": "cmd-xyz-456", "aggregateType": "Order", "aggregateId": "12345", "aggregateVersion": 7, "payload": { "orderId": 12345, "userId": 67890, "carrier": "DHL", "trackingCode": "JD123456789DE", "estimatedDelivery": "2025-03-31", "items": [ {"sku": "PROD-001", "quantity": 2, "warehouseId": "WH-MSK"} ] } } | Поле конверта | Описание |
|---|---|
| eventId | ULID или UUID для идемпотентности |
| eventType | Иерархический: domain.aggregate.action |
| eventVersion | Semantic versioning схемы payload |
| occurredAt | UTC ISO 8601 |
| correlationId | Для трассировки цепочки запросов |
| aggregateId + aggregateVersion | Для оптимистичной блокировки |
Avro-схема с эволюцией
{ "type": "record", "name": "OrderShipped", "namespace": "com.example.orders.events", "doc": "Событие отгрузки заказа со склада", "fields": [ {"name": "eventId", "type": "string"}, {"name": "eventType", "type": "string", "default": "order.shipped"}, {"name": "occurredAt", "type": {"type": "long", "logicalType": "timestamp-millis"}}, {"name": "orderId", "type": "long"}, {"name": "userId", "type": "long"}, {"name": "carrier", "type": "string"}, {"name": "trackingCode", "type": "string"}, { "name": "estimatedDelivery", "type": ["null", "string"], "default": null, "doc": "ISO date, может отсутствовать для некоторых перевозчиков" }, { "name": "warehouseId", "type": ["null", "string"], "default": null, "doc": "Добавлено в v1.1 — необязательное поле для backward compatibility" }, { "name": "shippingCost", "type": ["null", {"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2}], "default": null, "doc": "Добавлено в v1.2" } ] } Правила эволюции для backward compatibility:
- Новые поля — всегда с default (null или значение)
- Нельзя удалять обязательные поля
- Нельзя менять тип поля
- Нельзя переименовывать поля (добавьте alias, потом через мажорную версию переименуйте)
Чтобы добавить поле warehouseId без нарушения совместимости, укажите "default": null и тип ["null", "string"]. Тогда старые консьюмеры, не знающие об этом поле, просто получат null.
Как тестировать совместимость схем?
Мы автоматически проверяем обратную совместимость в CI/CD: сериализуем событие на стороне продюсера, десериализуем на стороне консьюмера и убеждаемся, что старые версии не падают. Это позволяет ловить breaking changes до деплоя. Контрактное тестирование фиксирует ожидания обеих сторон. Только один инцидент из-за несовместимости схем обходится в среднем в $15 000 при ночном деплое.
Как обеспечить обратную совместимость?
# Настройка Schema Registry — BACKWARD совместимость для всех событий orders curl -X PUT http://schema-registry:8081/config/order-events-value \ -H "Content-Type: application/vnd.schemaregistry.v1+json" \ -d '{"compatibility": "BACKWARD_TRANSITIVE"}' # BACKWARD_TRANSITIVE — новая схема совместима со ВСЕМИ предыдущими версиями, # не только с последней Мажорное изменение (breaking change) — новый топик:
-
order-events-v1→ для консьюмеров на старой схеме -
order-events-v2→ новая схема, консьюмеры мигрируют постепенно
Переходный период: продюсер публикует в оба топика. После полной миграции — order-events-v1 deprecated.
Event Catalog — документирование схем
Для команды из нескольких сервисов критично иметь центральный реестр событий. Используем AsyncAPI для описания каналов и сообщений.
asyncapi: 3.0.0 info: title: Order Service Events version: 1.0.0 description: События, публикуемые Order Service channels: order-events: address: order-events messages: OrderCreated: $ref: '#/components/messages/OrderCreated' OrderShipped: $ref: '#/components/messages/OrderShipped' OrderCancelled: $ref: '#/components/messages/OrderCancelled' components: messages: OrderCreated: name: OrderCreated title: Заказ создан summary: Публикуется при успешном создании нового заказа contentType: application/avro headers: type: object properties: correlationId: type: string description: ID входящего HTTP-запроса payload: type: object required: [eventId, orderId, userId, items, totalAmount] properties: eventId: type: string format: ulid orderId: type: integer format: int64 userId: type: integer format: int64 items: type: array items: type: object properties: sku: type: string quantity: type: integer price: type: number totalAmount: type: number createdAt: type: string format: date-time Типизированный Event Publisher (TypeScript/Node.js)
import { SchemaRegistry } from '@kafkajs/confluent-schema-registry'; import { Kafka } from 'kafkajs'; interface EventEnvelope<T> { eventId: string; eventType: string; eventVersion: string; occurredAt: string; producedBy: string; correlationId?: string; aggregateType: string; aggregateId: string; aggregateVersion: number; payload: T; } interface OrderShippedPayload { orderId: number; userId: number; carrier: string; trackingCode: string; estimatedDelivery?: string; } class OrderEventPublisher { private registry: SchemaRegistry; private producer: ReturnType<Kafka['producer']>; async publishOrderShipped(data: OrderShippedPayload, correlationId?: string): Promise<void> { const envelope: EventEnvelope<OrderShippedPayload> = { eventId: ulid(), eventType: 'order.shipped', eventVersion: '1.2', occurredAt: new Date().toISOString(), producedBy: 'order-service', correlationId, aggregateType: 'Order', aggregateId: String(data.orderId), aggregateVersion: await this.getAggregateVersion(data.orderId), payload: data, }; const schemaId = await this.registry.getLatestSchemaId('order-events-value'); const encoded = await this.registry.encode(schemaId, envelope); await this.producer.send({ topic: 'order-events', messages: [{ key: String(data.orderId), value: encoded, headers: { 'correlation-id': correlationId ?? '', 'event-type': 'order.shipped', }, }], }); } } Как проходит внедрение Event Schema?
В первый день проводим воркшоп с командами сервисов: составляем Event Storming карту, определяем все доменные события и их границы. На второй день разрабатываем Avro-схемы для каждого типа события, фиксируем правила именования и структуру конверта. Регистрируем их в Schema Registry. Третий день — реализация типизированных Event Publisher'ов в каждом сервисе-продюсере и создание AsyncAPI-документации. Четвёртый день — контрактные тесты, интеграция проверки совместимости в CI/CD и инструкция для команды по правилам эволюции схем.
Что входит в разработку схемы событий под ключ
- Event Storming воркшоп и документирование всех событий
- Avro-схемы с обратной совместимостью и версионированием
- Настройка Schema Registry (Kafka) с правилами BACKWARD_TRANSITIVE
- Типизированные Event Publisher'ы на TypeScript/Node.js или Java/Scala
- Библиотека для сериализации и десериализации событий
- AsyncAPI-спецификация для центрального реестра событий
- Контрактные тесты, интегрированные в CI/CD
- Документация для команды и обучение разработчиков
- Сопровождение на этапе миграции старых консьюмеров
Мы гарантируем качество результата: наш опыт — 7 лет в проектировании реактивных систем и микроядерной архитектуры, выполнено более 30 проектов по внедрению событийно-ориентированных интеграций. Получите консультацию по проектированию схем событий — мы оценим ваш проект и предложим оптимальное решение.







