Микросервисы обмениваются сообщениями через брокер. Без чёткого контракта любое изменение в формате события ломает потребителей. Однажды переименование поля в событии 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 проектов по внедрению событийно-ориентированных интеграций. Получите консультацию по проектированию схем событий — мы оценим ваш проект и предложим оптимальное решение.







