Preserving backward compatibility means keeping messages written with old Writer schemas readable by the new Reader schema. The BACKWARD setting on Confluent Schema Registry (especially the TRANSITIVE variant) is the linchpin.
This article is aligned with the CCDAK exam scope and focuses on precise terminology and concrete techniques you can apply on the job — safely adding fields, renaming them, and phasing them out incrementally.
Backward compatibility guarantees that a new Reader schema (typically on the new consumer side) can read data written with old Writer schemas. BACKWARD on Confluent Schema Registry expresses this property, and adding TRANSITIVE extends the guarantee to every version in the full history.
Conversely, when you want to ship a new Producer without breaking existing consumers, you need forward compatibility (FORWARD). FULL requires satisfying BACKWARD and FORWARD at the same time — it is the safest mode to maintain, but it narrows the set of allowed changes. In practice, you pick a level based on rollout order: are you updating consumers first, or producers first?
Based on Avro's reader/writer resolution rules, defaults on added fields, numeric type promotion, and aliases all help preserve compatibility. Protobuf and JSON Schema follow similar principles, but the details differ (for example, in Protobuf the field number is what matters).
| Compatibility Level | Read/Write Guarantee | Representative Safe Changes |
|---|---|---|
| BACKWARD_TRANSITIVE | New Reader can read every old Writer in the history | Add field (default required), add enum symbol, numeric type promotion (int → long/float/double, etc.), reorder fields, rename field + aliases |
| FORWARD_TRANSITIVE | Old Reader can read every new Writer in the history | Add field (in a form the old Reader can ignore), keep defaults stable; you cannot remove an existing required field |
| FULL_TRANSITIVE | Both sides can read each other across the entire history | Additions are strict (default required); absorb breaking changes through staged migrations |
Backward compatibility (BACKWARD_TRANSITIVE) flow
Setting the compatibility level (example: BACKWARD_TRANSITIVE)
curl -s -X PUT http://localhost:8081/config/t-value \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"compatibility": "BACKWARD_TRANSITIVE"}'
# グローバル既定を変える場合(推奨はSubject単位での設定)
# curl -s -X PUT http://localhost:8081/config -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"compatibility": "BACKWARD_TRANSITIVE"}'Adding a field is the safest and most common change. In Avro, always assign a default to the new field. Even when the old Writer omits the field, the new Reader can fall back to the default and still read the record.
Numeric type promotion (int → long/float/double, long → float/double, float → double) is valid for backward compatibility on the Reader side under Avro's resolution rules. Adding a new enum symbol is also backward compatible, but removing or reordering symbols requires care.
Renaming a field is breaking by default, but Avro's aliases let you map data written with the old name onto the field with the new name. In Protobuf, the field number is what matters more than the name, so renames are safe as long as the number stays the same.
Avro schema example: adding fields and renaming with aliases
// 旧スキーマ(Sv0)
{
"type": "record",
"name": "Order",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "string"},
{"name": "amount", "type": "int"},
{"name": "status", "type": {"type":"enum","name":"Status","symbols":["NEW","PAID"]}}
]
}
// 新スキーマ(Sr1, BACKWARD互換):フィールド追加+改名
{
"type": "record",
"name": "Order",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "string"},
{"name": "amount", "type": "long"}, // Reader側の型拡張
{"name": "state", "type": "string", "aliases": ["status"], "default": "NEW"},
{"name": "coupon", "type": ["null","string"], "default": null} // 新規追加
]
}
# 互換性チェック(最新と比較)
curl -s -X POST http://localhost:8081/compatibility/subjects/t-value/versions/latest \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"schema": "{ \"type\": \"record\", \"name\": \"Order\", ... }"}'Apparently breaking changes — removing a required field, narrowing a type, removing an enum symbol, or changing a Protobuf field number — should never be done in one shot. Decompose them into stages: first run the old and new shapes side by side in a backward-compatible form, then switch consumers, then producers, and finally remove what is no longer needed.
During migration, pin the Registry compatibility to BACKWARD_TRANSITIVE and only switch briefly to a FORWARD variant if you genuinely need a producer-first phase. Keep in mind that FULL narrows the set of allowed changes even further because it must satisfy both directions simultaneously.
Registration flow guarded by compatibility checks (pseudocode)
# 1) 互換性は原則 BACKWARD_TRANSITIVE
curl -s -X PUT http://localhost:8081/config/t-value -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"compatibility":"BACKWARD_TRANSITIVE"}'
# 2) 登録前に必ずドライラン確認
curl -s -X POST http://localhost:8081/compatibility/subjects/t-value/versions/latest \
-H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"schema": "...new schema..."}'
# 3) 問題なければ登録
curl -s -X POST http://localhost:8081/subjects/t-value/versions \
-H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"schema": "...new schema..."}'Compatibility is evaluated per subject. TopicNameStrategy (for example, topic-name-value) is the common choice and lets you manage compatibility independently per topic. If you want to share a record across multiple topics, RecordNameStrategy shares the compatibility history of identically named records — but watch out for unexpected collisions.
When you mix in multi-topic publishing or Kafka Connect/ksqlDB, confirm up front which subject will be registered (value vs. key, topic-driven vs. record-driven) and align your compatibility level and migration plan accordingly.
Configuring the subject name strategy on the Kafka Avro Serializer (example)
props.put("value.subject.name.strategy", "io.confluent.kafka.serializers.subject.TopicNameStrategy");
// 例: RecordNameStrategy を使う場合
// props.put("value.subject.name.strategy", "io.confluent.kafka.serializers.subject.RecordNameStrategy");Gate schema changes with the same rigor as code reviews. Run a dry-run check up front against the Registry's compatibility API, and automate a regression test that reads old-version messages with the new schema using fixture data. Comparing normalized schemas (absorbing field ordering and whitespace differences) keeps reviews focused on real changes.
In production, set the compatibility level explicitly on every subject. When you need to relax it as an exception, file a ticket that captures the time window, impact, and rollback procedure. Keeping an audit map from schema IDs to Git commits is also valuable.
Simple CI dry-run example (Bash)
SUBJECT="t-value"
NEWSCHEMA_FILE="schema_new.avsc"
BODY=$(jq -Rs '{schema: .}' < "$NEWSCHEMA_FILE")
curl -s -X POST "http://localhost:8081/compatibility/subjects/${SUBJECT}/versions/latest" \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d "$BODY" | jq .Backward compatibility lives or dies on consumer robustness. Define defaults on the new Reader schema, ignore unknown fields, and branch on nullability explicitly. Log-compacted topics may deliver tombstones (value=null), so make sure your code is null-safe whether you use Avro, JSON Schema, or Protobuf.
With GenericRecord, do existence checks and apply defaults yourself; with SpecificRecord, you rely on the defaults baked into the generated code. On errors, prefer routing messages to a dead-letter or retry queue over silently skipping them — that way schema and data mismatches stay observable.
Defensive read with Avro GenericRecord (Java, sketch)
GenericRecord r = consumerRecord.value();
Long amount = null;
if (r.getSchema().getField("amount") != null) {
Object v = r.get("amount");
if (v instanceof Integer) amount = ((Integer) v).longValue();
else if (v instanceof Long) amount = (Long) v;
}
String state = (String) (r.get("state") != null ? r.get("state") : "NEW");
// coupon は optional
String coupon = r.get("coupon") == null ? null : r.get("coupon").toString();CCDAK
問題 1
You add a new optional field with a default value to the value schema in Avro. You want to update consumers to the new Reader schema first and still read older data. Which compatibility level on the Schema Registry subject is most appropriate (covering the entire history)?
正解: A
You need a guarantee that the new Reader can read every Writer in the full history, which is exactly what BACKWARD_TRANSITIVE provides. FORWARD guarantees the opposite direction (old Reader reads new Writer), NONE guarantees nothing, and FULL imposes the bidirectional constraint you do not need here.
When should I use BACKWARD vs FORWARD compatibility?
Choose BACKWARD (new Reader reads old Writer) when you plan to roll out consumers first, and FORWARD (old Reader reads new Writer) when you plan to roll out producers first. Reserve FULL for the unusual case where you need both guarantees at once.
How can I safely rename a field?
In Avro, register the old name as an alias on the new field. Migrate consumers to the new name in stages and only drop the field with the old name after a sufficient grace period. In Protobuf, field numbers are what matter, so renaming is safe as long as you keep the same field number.
What should I watch out for when changing an enum?
Adding a new symbol is backward compatible, but removing or replacing an existing symbol fails in most compatibility modes. Changing a symbol's meaning is also effectively a breaking change. Stick to additions and treat unused values as deprecated at the application layer.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
Kafka Topics & Partitions: Distribution Fundamentals (2026)
How Kafka topics and partitions enable scale — ordering guar...
CCDAK Exam Guide: Confluent Certified Developer (2026)
Complete prep for the CCDAK exam — Producer/Consumer API, St...
CCAAK Exam Guide: Confluent Certified Administrator (2026)
Pass the CCAAK exam — cluster management, partitions, securi...
Kafka Replicas & ISR: Fault Tolerance Explained (2026)
Replica placement, in-sync replicas (ISR), leader election. ...
Kafka Offsets: Commit Modes & Consumer Position (2026)
Offset semantics — auto vs. manual commit, __consumer_offset...