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







