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







