Schema drift is one of the quietest ways to break a production pipeline. A producer team adds a field, removes one they thought was unused, or renames a type in their internal model. The change passes code review, deploys cleanly, and the producer metrics look fine. Meanwhile, a consumer on the other end starts throwing deserialization errors, or worse, silently drops values and keeps running. By the time anyone notices, the damage is already in downstream aggregations, dashboards, or audit logs.
This post covers the mechanics of event streaming schema evolution systematically: how a schema registry enforces Kafka schema compatibility, which compatibility mode fits which operational pattern, how upgrade order interacts with those modes, and how Avro, Protobuf, and JSON Schema each behave under the same rules. Producer-consumer contract management is not a nice-to-have; it is the discipline that separates a real-time pipeline from a time bomb.
Why Kafka needs an explicit contract layer
Kafka itself is format-agnostic. It transfers bytes and does not validate what those bytes mean. Producers and consumers must agree on a common data type independently, and in a system where services deploy on different schedules, that agreement degrades over time unless something enforces it.
A schema registry is that enforcement layer. It sits between producers and consumers, holds a versioned history of every schema, and checks compatibility before accepting a new version. When a producer publishes a message, it embeds the schema ID at the start of the payload. The consumer reads that ID, fetches the corresponding schema from the registry (caching it locally), and deserializes accordingly. The registry does not just store schemas; it is the mechanism that makes rolling upgrades safe.
The seven compatibility modes, and what they actually mean
Confluent’s Schema Registry defines seven valid compatibility levels: BACKWARD, BACKWARD_TRANSITIVE, FORWARD, FORWARD_TRANSITIVE, FULL, FULL_TRANSITIVE, and NONE. Most teams only think about three, which is how they end up in trouble with the other four.
Backward (the default)
BACKWARD is the Schema Registry default and the most common starting point. A new schema version is compatible with data written by the previous version. Concretely, you can delete fields and add fields, as long as new fields carry a default value. The critical operational rule: upgrade consumers before producers. The new consumer must be able to read old messages before the producer starts writing new ones.
The main reason BACKWARD is the default for Kafka is that it lets you rewind consumers to the beginning of a topic and replay historical data against a new schema. That property is non-negotiable for pipelines that need replay for recovery or backfill.
Forward
FORWARD inverts the relationship: old consumers can read data written by a new producer. You can delete fields that have defaults and add new fields, but you are not guaranteed the ability to read old messages with a new schema. Upgrade producers first. Forward compatibility is harder to sustain because it requires anticipating all future changes at the time you design the schema. In practice, teams that use it tend to have stable, well-understood message shapes.
Full
FULL requires a schema to be both backward and forward compatible simultaneously. The only changes allowed are adding or deleting optional fields, meaning fields that carry default values in both directions. The payoff is that you can upgrade producers and consumers in any order, which removes the coordination cost entirely. For teams running many independent services against a shared topic, full compatibility is often worth the constraint.
None
NONE disables all compatibility checking. The registry accepts any schema version regardless of what changed. This is not a shortcut; the coordination burden shifts entirely to the teams involved, and in most cases requires updating producers and consumers simultaneously. Use it only when you have explicit orchestration tooling and a clear reason.
Transitive variants: the overlooked dimension
Non-transitive compatibility checks only against the latest schema version. Transitive compatibility checks against every historical version. The difference is invisible until you have accumulated enough small, individually safe changes to break a consumer that skipped several versions.
The non-transitive modes (BACKWARD, FORWARD, FULL) validate a new schema only against the immediately preceding version. The transitive variants check against all historical versions. For long-lived systems where consumers may be several versions behind, transitive modes prevent the accumulation of changes that are each locally safe but collectively incompatible. One practical note: if you rely on a default value in transitive mode, that default must have been declared since the very first version of the schema.
One additional constraint worth noting: Kafka Streams supports only BACKWARD compatibility. If your topology uses Kafka Streams for stateful processing, that choice is made for you.
Format-specific behaviour under the same rules
The compatibility modes are format-agnostic at the registry level, but the serialization format determines which changes are physically possible within each mode.
Avro
Avro is the most common choice for schema evolution flexibility. New fields can be added backward-compatibly as long as they carry a default value. Fields are made optional using a union type that includes null. Avro does not require code generation, which reduces friction in teams that iterate on schemas frequently. It is the right choice when schemas change often and you need the registry to do the heavy lifting.
Protobuf
Protobuf’s evolution model is built around field numbers rather than field names. Field numbers must remain stable; removed fields must be marked with the reserved keyword to prevent reuse of their tag. Unknown fields are ignored during deserialization, which makes adding new optional fields safe for old consumers. Protobuf produces smaller payloads and performs well across language boundaries, but it requires code generation and more discipline around field number management.
JSON Schema
JSON Schema is the most readable format but the most fragile under evolution. The key gotcha is the open content model: a schema that allows additional properties will treat a new property definition as a backward-incompatible change if old data carries a property of the same name with a different type. JSON Schema is appropriate when human readability matters more than efficiency, or when integrating with REST APIs that already use JSON, but it is a poor fit for high-throughput pipelines that evolve frequently.
Subject naming strategies and multi-type topics
Schema Registry organises schemas by subject, and the naming strategy determines how subjects map to topics. The default is TopicNameStrategy: the subject name is the topic name suffixed with -key or -value. This works cleanly when a topic carries a single message type.
When a topic carries multiple event types, TopicNameStrategy breaks down because all messages in the topic share one schema subject. RecordNameStrategy uses the fully qualified record name as the subject, decoupling schema versioning from topic structure. TopicRecordNameStrategy combines both, scoping the record name to a specific topic. Choosing the right strategy upfront is part of schema registry best practices; retrofitting it later requires coordinated migration.
Running this in production
We run NATS JetStream as our own internal event-streaming backbone, and we operate Kafka-compatible infrastructure for customers under the same operational standards that govern the rest of our platform: Tier III facilities, ISO 27001 certification, and a contractual 99.9% uptime SLA that is a written commitment, not a marketing line. Schema governance is part of the pipeline design conversation we have with customers before they onboard, not an afterthought when something breaks.
The practical discipline for any team managing schema evolution at scale comes down to a short list: pick a compatibility mode that matches your deployment model and stick to it, use transitive variants for any topic with a long retention window, define defaults for every field you might want to remove later, and treat the upgrade order rule for your chosen mode as a hard deployment gate rather than a guideline. Schema drift is preventable; it just requires treating the producer-consumer contract with the same rigour you give to API versioning.




