Data Engineering glossary
What is a Schema Registry?
A schema registry is a service that stores the versioned schemas of the messages on Kafka topics and checks every new version for compatibility before producers can use it. Messages carry only a small schema ID, and consumers fetch the schema from the registry, so producers and consumers can evolve independently without breaking each other.
How it works
Schemas are registered under a subject, usually <topic>-value and <topic>-key. When a producer serializes a message, its serializer registers or looks up the schema and writes a 5-byte header (a magic byte and the schema ID) before the Avro, Protobuf or JSON payload. A consumer reads the ID, fetches and caches that schema, and decodes the message, resolving it against its own reader schema.
The registry enforces a compatibility mode per subject:
| Mode | Guarantee | Safe changes (Avro) |
|---|---|---|
| BACKWARD (default) | New consumers can read old data | Delete fields; add fields with defaults |
| FORWARD | Old consumers can read new data | Add fields; delete fields with defaults |
| FULL | Both directions | Add or delete fields with defaults |
_TRANSITIVE variants |
Against all versions, not just the last | Same, checked against history |
BACKWARD lets you upgrade consumers first; FORWARD lets you upgrade producers first. A registration that breaks the mode is rejected with HTTP 409, which stops a breaking change at deploy time instead of in production. Common implementations are Confluent Schema Registry, AWS Glue Schema Registry, Apicurio and the registries built into managed Kafka services.
Example
A BACKWARD compatibility check: data written with version 1 is read with a candidate version 2. Adding tier with a default works; adding it without a default fails, which is the kind of change a registry rejects (Python 3.11, fastavro 1.13).
import io
from fastavro import parse_schema, schemaless_reader, schemaless_writer
v1 = {"type": "record", "name": "Customer", "fields": [
{"name": "id", "type": "long"},
{"name": "city", "type": "string"}]}
v2_ok = {"type": "record", "name": "Customer", "fields": [
{"name": "id", "type": "long"},
{"name": "city", "type": "string"},
{"name": "tier", "type": "string", "default": "standard"}]} # new field WITH a default
v2_bad = {"type": "record", "name": "Customer", "fields": [
{"name": "id", "type": "long"},
{"name": "city", "type": "string"},
{"name": "tier", "type": "string"}]} # new field, no default
def backward_compatible(new, old):
"""BACKWARD check: can a consumer on `new` read a message written with `old`?"""
buf = io.BytesIO()
schemaless_writer(buf, parse_schema(old), {"id": 7, "city": "Pune"})
buf.seek(0)
try:
return schemaless_reader(buf, parse_schema(old), parse_schema(new))
except Exception as e:
return f"incompatible ({type(e).__name__})"
print("v2 with default:", backward_compatible(v2_ok, v1))
print("v2 without default:", backward_compatible(v2_bad, v1))
v2 with default: {'id': 7, 'city': 'Pune', 'tier': 'standard'}
v2 without default: incompatible (SchemaResolutionError)
Common mistakes
- Setting compatibility to NONE to get a deploy through, which lets breaking changes reach consumers.
- Using BACKWARD while upgrading producers before consumers, the order that mode does not protect.
- Auto-registering schemas from every producer build in production instead of registering through CI.
- Deleting old schema versions that messages still on the topic refer to.

