Standardizing Microservice Events: Avro, Schema Registry, and Contracts
Without a clear contract, any change in event format breaks consumers. Once renaming a field in the OrderShipped event led to three services crashing at 3:00 AM. Recovery took 4 hours, and the average loss from such an incident is $15,000. Event Schema is a contract that ensures all changes are explicit and controlled. It prevents 80% of compatibility issues before deployment. Contact us — we will help design schemas for your architecture and eliminate nighttime crashes.
Avro with Schema Registry is 3x faster in serialization than JSON without a schema and provides strict typing. We guarantee backward compatibility at all stages of evolution — so you avoid nighttime incidents and accelerate development.
Why Avro and Not JSON?
Avro is a binary serialization format that is 3-5 times more compact than JSON. It is strictly typed: a field can only be of the type described in the schema. Schema Registry automatically checks the compatibility of new schemas against old ones: if you add a required field without a default, the registry will reject registration. JSON does not provide such guarantees, and compatibility errors are only discovered at runtime.
| Characteristic | Avro | JSON (without schema) |
|---|---|---|
| Typing | strict | dynamic |
| Message size | compact (binary) | verbose (text) |
| Compatibility | automatic (Schema Registry) | manual |
| Serialization speed | high (up to 3x faster) | low |
| Evolution support | built-in (default, alias) | none |
What Principles Underlie Event Schema?
Events describe facts, not commands. OrderShipped is a fact. ShipOrder is a command. An event has occurred and cannot be undone (only compensated by another event).
The schema should be self-contained. The consumer should not make additional requests to process the event. All necessary data is in the event body.
Backward compatibility by default. Old consumers must work with new events without changes.
Event Structure
{
"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"}
]
}
}
| Envelope Field | Description |
|---|---|
| eventId | ULID or UUID for idempotency |
| eventType | Hierarchical: domain.aggregate.action |
| eventVersion | Semantic versioning of payload schema |
| occurredAt | UTC ISO 8601 |
| correlationId | For tracing request chains |
| aggregateId + aggregateVersion | For optimistic locking |
Avro Schema with Evolution
{
"type": "record",
"name": "OrderShipped",
"namespace": "com.example.orders.events",
"doc": "Event of order shipment from warehouse",
"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, may be absent for some carriers"
},
{
"name": "warehouseId",
"type": ["null", "string"],
"default": null,
"doc": "Added in v1.1 — optional field for backward compatibility"
},
{
"name": "shippingCost",
"type": ["null", {"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2}],
"default": null,
"doc": "Added in v1.2"
}
]
}
Evolution rules for backward compatibility:
- New fields — always with default (null or value)
- Cannot remove required fields
- Cannot change field type
- Cannot rename fields (add alias, then rename via major version)
To add a field warehouseId without breaking compatibility, use "default": null and type ["null", "string"]. Old consumers that do not know about this field will simply receive null.
How to Test Schema Compatibility?
We automatically check backward compatibility in CI/CD: serialize the event on the producer side, deserialize on the consumer side, and verify that old versions do not crash. This catches breaking changes before deployment. Contract testing fixes expectations on both sides. Just one incident due to schema incompatibility costs an average of $15,000 during a nightly deploy.
How to Ensure Backward Compatibility?
# Configure Schema Registry — BACKWARD compatibility for all order events
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 — new schema compatible with ALL previous versions,
# not just the latest
Major change (breaking change) — new topic:
-
order-events-v1→ for consumers on the old schema -
order-events-v2→ new schema, consumers migrate gradually
Transition period: producer publishes to both topics. After full migration, order-events-v1 deprecated.
Event Catalog — Documenting Schemas
For a team with multiple services, a central event registry is critical. We use AsyncAPI to describe channels and messages.
asyncapi: 3.0.0
info:
title: Order Service Events
version: 1.0.0
description: Events published by 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: Order created
summary: Published when a new order is successfully created
contentType: application/avro
headers:
type: object
properties:
correlationId:
type: string
description: ID of the incoming HTTP request
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
Typed 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',
},
}],
});
}
}
How Does Event Schema Implementation Work?
Day 1: we hold a workshop with service teams: create an Event Storming map, identify all domain events and their boundaries. Day 2: develop Avro schemas for each event type, define naming rules and envelope structure. Register them in Schema Registry. Day 3: implement typed Event Publishers in each producer service and create AsyncAPI documentation. Day 4: contract tests, integrate compatibility checks into CI/CD, and provide the team with instructions on schema evolution rules.
What's Included in Turnkey Event Schema Development
- Event Storming workshop and documentation of all events
- Avro schemas with backward compatibility and versioning
- Schema Registry (Kafka) configuration with BACKWARD_TRANSITIVE rules
- Typed Event Publishers in TypeScript/Node.js or Java/Scala
- Serialization/deserialization library for events
- AsyncAPI specification for the central event registry
- Contract tests integrated into CI/CD
- Team documentation and developer training
- Support during migration of old consumers
We guarantee quality of results: our experience is 7 years in designing reactive systems and microkernel architecture, completed over 30 projects implementing event-driven integrations. Get a consultation on event schema design — we will evaluate your project and propose the optimal solution.







