SD System Design In Depth
0%
lesson system-designeventscontracts

Event contracts

Module 3.1: Network Protocols & Service Communication Updated Sep 17, 2026
📄 Cheat Sheet
How to Evolve your Microservice Schemas | Designing Event-Driven Microservices
Confluent, an IBM Company How to Evolve your Microservice Schemas | Designing Event-Driven Microservices Watch on YouTube ↗

Focused explanation of evolving event schemas without breaking consumers: compatible changes, versioned events and when to publish a new topic, exactly this unit's concern.

0:00 / --:--

Under the Hood: Asynchronous Schema Evolution

In synchronous APIs, if a client sends an invalid field, the server returns an immediate HTTP 400. In asynchronous event-driven architectures, once an event is published to Kafka or RabbitMQ, it is persisted to disk and consumed by dozens of downstream microservices hours or days later. A malformed or incompatible event stays in the log: consumers can stall on it as a poison message unless they route it to a dead-letter topic, and every replay hits it again. Event Contracts formalize asynchronous payloads.

Schema Registry Enforcement Flow

flowchart LR Producer["Producer: OrderService"] -->|"1. Validate against Avro/Protobuf Schema"| Registry[("Schema Registry")] Registry -->|"2. Return Schema ID, cached by the client"| Producer Producer -->|"3. Magic byte + 4-byte Schema ID + Payload to Kafka"| Kafka[("Kafka Broker")] Kafka -->|"4. Consume Event"| Consumer["Consumer: BillingService"] Consumer -->|"5. Fetch Schema by ID, then cache"| Registry

Schema Evolution Compatibility Modes

  • Backward Compatibility (Default): Newer schema code can read events written with older schemas. Consumers must be upgraded before producers. New fields must have default values.
  • Forward Compatibility: Older schema code can read events written with newer schemas. Producers must be upgraded before consumers. Fields may only be deleted if they had defaults.
  • Full Compatibility: Schemas are both backward and forward compatible. Old and new producers and consumers can operate concurrently in any upgrade order without downtime.

Worked example: adding a field to OrderPlaced

Version 1 of the Avro schema for OrderPlaced has order_id, customer_id and total_cents. The team wants to add currency.

  • Add with a default ("currency", type string, default "USD"). New consumers reading old events fill in "USD" (backward compatible). Old consumers reading new events ignore the unknown field (forward compatible). So this change is fully compatible and can be deployed in any order.
  • Add without a default: new consumers cannot read old events that lack the field, so Schema Registry rejects it under BACKWARD mode.
  • Rename total_cents to amount: not compatible in either direction. Add amount as a new field, dual-populate both for a migration period, then remove the old one only when no consumer reads it, or publish OrderPlaced.v2 to a new topic.

On the wire, Confluent's serializers prefix each Kafka message with 1 magic byte (0) and a 4-byte schema ID. The consumer looks up the writer's schema by ID (and caches it), then resolves it against its own reader schema. That is what lets old and new versions coexist in one topic.

Compatibility modes

ModeGuaranteeAllowed changes (Avro)Upgrade order
BACKWARD (Confluent default)New schema can read data written with the previous schemaDelete fields, add fields with defaultsConsumers first
FORWARDPrevious schema can read data written with the new schemaAdd fields, delete fields that had defaultsProducers first
FULLBoth directionsAdd or delete only fields with defaultsAny order
*_TRANSITIVESame, checked against all earlier versions, not just the latestSame as aboveNeeded when consumers replay old history
NONENo checksAnythingCoordinated deploys only

What else belongs in the contract

  • Identity and deduplication: a unique event ID so at-least-once delivery can be deduplicated.
  • Ordering key: which key determines the partition, and therefore which events are ordered relative to each other (for example all events for one order_id).
  • Semantics: whether the event is a notification (go fetch the details) or carries full state, and whether it can be replayed safely.
  • Retention and privacy: how long events are kept and which fields contain personal data that must be deletable.

Failure modes

  • Poison messages: route events that fail deserialization or validation to a dead-letter topic with the error, and alert.
  • Semantic breaks that pass schema checks, such as changing units or meaning; review them like breaking API changes.
  • Protobuf evolution: never reuse a field number; reserve removed numbers and names.
Check yourself Question 1 of 3
Upgrade order for BACKWARD recall
From: Schema Evolution Compatibility Modes

Under BACKWARD compatibility (Confluent's default), which side must be upgraded first when a schema changes, and why?

Key Takeaways

Asynchronous event schemas must be strictly governed by a Schema Registry to avoid poisoning consumer queues.
Use Avro or Protocol Buffers to serialize schemas with compact integer IDs prepended to wire payloads.
Full Compatibility guarantees that producers and consumers can be deployed independently in any order.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue