Service A writes a Kafka message with field user_id. Service B reads it. Service A’s team renames it to userId next sprint. Service B starts throwing deserialization errors at runtime. Neither team knew about the other.

The Problem#

In a microservices system passing messages through Kafka, producers and consumers evolve independently. There’s no enforced contract. A producer can change a field name, add a required field, or change a data type, and the consumer finds out when deserialization fails in production.

A schema registry solves this by storing the schema for every message type in a central repository. Producers register their schema before sending. Consumers fetch the schema before deserializing. Compatibility rules are enforced at registration time: if your new schema breaks existing consumers, the registry rejects it before you can even produce a message.

How It Works#

Each Kafka message carries a schema ID in its header (a 4-byte integer). The message payload is compact binary, Avro or Protobuf rather than JSON. The consumer reads the schema ID from the header, fetches the schema from the registry by ID, then deserializes the payload. Schema lookups are cached locally. The hot path doesn’t hit the registry on every message.

graph TD A[Producer: register schema v1] --> B[Schema Registry] B --> C[Assign schema ID: 42] A --> D[Write Kafka message: header=42, body=Avro bytes] D --> E[Kafka Topic] E --> F[Consumer: read message] F --> G[Extract schema ID: 42] G --> H[Fetch schema 42 - or use local cache] H --> I[Deserialize payload] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style H fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style I fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Compatibility Modes#

The registry organizes schemas by subject, typically one per Kafka topic. When you register a new schema version, the registry checks compatibility against previous versions. Backward compatibility means new consumers can read old messages. Forward compatibility means old consumers can read new messages. Full compatibility requires both. Most teams start with backward and only realize they need forward when they try to reprocess old messages with new consumer code.

Confluent Schema Registry is the most common implementation. It exposes a REST API and stores schemas in an internal Kafka topic.

At Salesforce#

We had a message bus where events were serialized as JSON with no schema enforcement. After a year, two different services were producing opportunity_created events with different field names for the same concept. Consumer code had conditional logic to handle both variants. Introducing Avro schemas with compatibility enforcement caught five incompatible schema changes within the first month, before they reached production.

What I’m Learning#

The schema registry is only as useful as the compatibility mode you enforce. Setting it to “none” gives you a registry with no guardrails, which is almost worse than not having one, because it creates false confidence.

What schema format are you using across your Kafka topics, and do you have compatibility enforcement in place?