Event Schema Governance
Schema Registry Avro JSON Schema Protobuf and Compatibility Mode
Schema governance untuk event-driven systems: schema registry, Avro, JSON Schema, Protobuf, compatibility mode, schema evolution, validation, producer/consumer compatibility, and production review
Part 074 — Schema Registry, Avro, JSON Schema, Protobuf, and Compatibility Mode
Fokus part ini: memahami schema governance untuk event-driven systems. Kita akan membahas schema registry, Avro, JSON Schema, Protobuf, compatibility mode, schema evolution, validation, dan bagaimana mereview perubahan event agar producer/consumer tidak rusak.
Kafka hanya menyimpan bytes.
key: bytes
value: bytes
headers: key-value bytes
Kafka tidak tahu apakah payload adalah:
- Avro
- JSON
- Protobuf
- String
- encrypted blob
- corrupted bytes
Tanpa schema governance, event-driven system berubah menjadi kontrak informal yang rapuh.
Masalah biasanya tidak muncul saat producer dan consumer dikembangkan oleh orang yang sama. Masalah muncul ketika:
- consumer lama masih berjalan
- producer baru deploy lebih dulu
- event field dihapus
- enum value baru ditambahkan
- numeric type berubah
- JSON field rename tanpa compatibility plan
- replay event lama dengan schema baru
- generated class berbeda antar service
Schema governance adalah cara menjaga event sebagai contract, bukan side effect internal.
1. Core Mental Model
Event schema adalah kontrak antar waktu dan antar service.
producer version N writes event
consumer version N consumes event
consumer version N-1 may still consume event
consumer version N+1 may replay old event
analytics/backfill may read event months later
Karena itu compatibility harus dipikirkan dalam dua arah:
new consumer reads old event
old consumer reads new event
Schema registry membantu menjawab:
- schema apa yang valid untuk topic/event ini?
- apakah schema baru compatible dengan schema lama?
- schema version mana yang digunakan payload ini?
- siapa owner schema?
- apakah producer boleh publish payload ini?
- apakah consumer bisa deserialize payload lama?
2. Schema Registry Concept
Schema registry adalah service/repository untuk menyimpan schema dan compatibility rule.
producer
-> register/lookup schema
-> serialize payload with schema id/version
-> publish bytes to Kafka
consumer
-> read bytes
-> extract schema id/version
-> fetch schema if needed
-> deserialize payload
Biasanya payload membawa schema ID dalam wire format atau header.
Kafka record value:
magic byte + schema id + encoded payload
Atau:
headers:
schema-id: 123
schema-version: 4
content-type: application/avro
Exact implementation bergantung library/platform.
3. Subject Naming Strategy
Schema registry biasanya menggunakan subject.
Subject menentukan compatibility scope.
Contoh subject:
quote.events-value
quote.events-key
QuoteApproved-value
tenant.quote.approved-value
Trade-off:
Topic-based subject
<topic>-value
Kelebihan:
- sederhana
- satu topic satu schema family
- mudah dipahami tooling
Kekurangan:
- sulit jika satu topic berisi banyak event type dengan schema berbeda
Record/event-based subject
<event-name>-value
Kelebihan:
- cocok untuk event catalog
- compatibility per event type
Kekurangan:
- butuh routing/metadata lebih jelas
- topic bisa memuat banyak schema ID
Topic + event subject
<topic>-<event-name>-value
Kelebihan:
- eksplisit
- mengurangi collision
Kekurangan:
- lebih verbose
- butuh governance naming
Internal standard harus diverifikasi.
4. Avro
Avro populer di Kafka karena compact binary encoding dan schema evolution support.
Karakteristik:
- schema dalam JSON format
- binary payload compact
- mendukung reader schema vs writer schema
- default value penting untuk compatibility
- union type digunakan untuk nullable field
- sering dipakai dengan schema registry
Example Avro schema:
{
"type": "record",
"name": "QuoteApproved",
"namespace": "com.example.quote.events",
"fields": [
{ "name": "eventId", "type": "string" },
{ "name": "quoteId", "type": "string" },
{ "name": "aggregateVersion", "type": "long" },
{ "name": "approvedAt", "type": { "type": "long", "logicalType": "timestamp-millis" } },
{ "name": "approvedBy", "type": ["null", "string"], "default": null }
]
}
Avro Compatibility Rules
Adding optional field with default is usually compatible.
{ "name": "reason", "type": ["null", "string"], "default": null }
Removing required field can break consumers.
Renaming field is usually breaking unless alias is used.
Changing type can break unless promotion is supported and safe.
Important Avro concerns:
- default values are not optional detail; they are compatibility mechanism
- logical types must be standardized
- enum evolution must be reviewed carefully
- generated class compatibility depends on build tooling
5. JSON Schema
JSON Schema is useful when payload is JSON and human-readable.
Characteristics:
- easier to inspect manually
- aligns with JSON APIs
- flexible but can become too loose
- compatibility depends on strict schema discipline
Example JSON Schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "QuoteApproved",
"type": "object",
"required": ["eventId", "quoteId", "aggregateVersion", "approvedAt"],
"properties": {
"eventId": { "type": "string" },
"quoteId": { "type": "string" },
"aggregateVersion": { "type": "integer", "minimum": 1 },
"approvedAt": { "type": "string", "format": "date-time" },
"approvedBy": { "type": ["string", "null"] }
},
"additionalProperties": false
}
JSON Schema Trade-Off
additionalProperties: false gives stricter validation but can hurt forward compatibility if old consumers reject new fields.
additionalProperties: true gives forward compatibility but can hide typos and contract drift.
Senior review question:
Should unknown fields be tolerated at event boundary?
For external/public contracts, tolerance can be useful. For internal event governance, strictness plus compatibility checks can catch mistakes earlier.
6. Protobuf
Protobuf is compact, strongly typed, and common for gRPC and event contracts.
Characteristics:
- field numbers are part of contract
- field names can change more safely than field numbers, but generated APIs are affected
- removing fields requires reserving field numbers/names
- unknown fields may be preserved depending on runtime
- good for cross-language contracts
Example Protobuf:
syntax = "proto3";
package quote.events.v1;
message QuoteApproved {
string event_id = 1;
string quote_id = 2;
int64 aggregate_version = 3;
string approved_at = 4;
optional string approved_by = 5;
}
Protobuf Compatibility Rules
Do not reuse field numbers.
Bad:
// old
string approved_by = 5;
// new, bad reuse
string sales_channel = 5;
Better:
reserved 5;
reserved "approved_by";
string sales_channel = 6;
Changing field type can be breaking. Changing meaning of field without changing schema is also breaking, even if binary compatibility passes.
7. Compatibility Modes
Common compatibility modes:
BACKWARD
FORWARD
FULL
NONE
BACKWARD_TRANSITIVE
FORWARD_TRANSITIVE
FULL_TRANSITIVE
Exact names may differ by registry implementation.
7.1 Backward Compatibility
New consumer can read old data.
consumer v2 reads event produced with schema v1
Useful for replay old data using new code.
7.2 Forward Compatibility
Old consumer can read new data.
consumer v1 reads event produced with schema v2
Useful during rolling deployment where producer upgrades before all consumers.
7.3 Full Compatibility
Both backward and forward.
consumer v2 reads v1
consumer v1 reads v2
Safer but more restrictive.
7.4 Transitive Compatibility
Schema vN must be compatible with all previous versions, not only vN-1.
This matters for long-lived topics and replay.
v4 compatible with v1, v2, v3
For enterprise event streams with replay requirement, transitive compatibility is often more defensible.
8. Schema Evolution Patterns
8.1 Add Optional Field
Usually safe if default exists or consumer tolerates missing value.
"discountReason": null
Review:
- default defined?
- old consumers tolerate unknown field?
- generated classes updated?
- analytics consumers aware?
8.2 Add Required Field
Usually breaking.
Old events do not have the field. Old producers cannot populate it.
Safer path:
- Add optional field.
- Deploy consumers that tolerate/populate default.
- Deploy producers.
- Backfill if needed.
- Later enforce required only if all historical/replay concerns resolved.
8.3 Rename Field
Usually breaking.
Safer path:
- Add new field.
- Produce both old and new field for transition if format allows.
- Migrate consumers.
- Deprecate old field.
- Remove only after policy window.
8.4 Remove Field
Can break old consumers.
Safer path:
- mark deprecated
- keep producing default/null
- remove after consumer inventory confirms no usage
8.5 Change Type
High risk.
Example:
int -> long may be safe in some formats
string -> object is breaking
number -> string is semantic breaking
8.6 Add Enum Value
Often underestimated.
Old consumer may fail on unknown enum.
Safer strategy:
- consumer default/UNKNOWN branch
- schema evolution test
- deploy consumers before producer emits new value
9. Event Envelope vs Event Payload Schema
Separate envelope fields from domain payload fields.
Envelope:
{
"eventId": "evt-123",
"eventType": "QuoteApproved",
"eventVersion": 1,
"aggregateId": "quote-456",
"aggregateVersion": 17,
"tenantId": "tenant-a",
"occurredAt": "2026-07-10T04:00:00Z",
"producer": "quote-service",
"correlationId": "corr-789",
"causationId": "cmd-555"
}
Payload:
{
"approvedBy": "user-123",
"approvalReason": "commercial-approved"
}
Why separate?
- envelope supports routing, idempotency, ordering, observability
- payload contains business fact
- envelope evolves slower
- payload evolves per event type
Possible designs:
- Envelope and payload in one schema.
- Envelope schema + payload schema nested.
- Metadata in Kafka headers + value contains payload.
Each has trade-off.
Headers are useful for routing/tracing but are easier to lose in some tooling/replay paths. Critical business metadata should usually be in payload/envelope, not only headers.
10. Producer Compatibility
Producer compatibility asks:
Can this producer publish records that all expected consumers can read?
Producer-side checks:
- schema registered before publish
- schema compatibility validated in CI
- serializer uses correct subject
- required envelope fields populated
- event type/version matches schema
- default/null semantics understood
- generated class version matches schema artifact
Bad producer behavior:
- publish raw JSON without schema validation
- use
Map<String,Object>without contract - add enum value without consumer rollout
- change field meaning without schema change
- publish event before DB transaction commits
11. Consumer Compatibility
Consumer compatibility asks:
Can this consumer read old and new event versions safely?
Consumer-side checks:
- unknown field tolerance policy
- missing field default policy
- unknown enum handling
- old event replay tested
- event version branching if needed
- schema ID resolution works offline/cache failure
- poison message classification for incompatible schema
Consumer should not assume latest schema only.
Replay means old events can reappear.
12. Contract Compatibility Matrix
A compatibility matrix makes rollout risk explicit.
Example:
| Producer | Consumer | Expected Result |
|---|---|---|
| v1 | v1 | OK |
| v1 | v2 | backward compatibility required |
| v2 | v1 | forward compatibility required |
| v2 | v2 | OK |
| v3 | v1 | maybe unsupported after deprecation window |
For event changes, also include:
| Scenario | Required? |
|---|---|
| new consumer reads old topic data | yes/no |
| old consumer reads new producer event during rolling deploy | yes/no |
| DLQ replay after 90 days | yes/no |
| analytics reads historical events | yes/no |
| cross-region replication preserves schema ID | yes/no |
Matrix should be part of architecture review for breaking changes.
13. Generated Code and Build Governance
Schema often generates Java classes.
Risks:
- generated classes checked into repo vs generated during build
- generator version drift
- schema artifact version mismatch
- incompatible generated API after field rename
- consumer uses stale generated class
Build governance:
schema artifact -> code generation -> compile -> compatibility test -> publish artifact
Checklist:
- generator version pinned
- generated code reproducible
- schema artifact versioned
- compatibility tests run in CI
- no manual edit to generated code
- generated package naming stable
14. Schema Validation in CI/CD
Schema compatibility should fail before deployment.
Pipeline checks:
lint schema
validate naming
validate required envelope fields
validate compatibility mode
generate code
run unit tests
run consumer contract tests
publish schema artifact
Bad pattern:
producer registers schema for first time in production at startup
Risk:
- production startup fails due to compatibility rejection
- producer deploy partially succeeds
- emergency rollback complicated
Better:
schema registration/validation happens in controlled pipeline stage
Runtime may still look up schema, but compatibility surprises should be caught earlier.
15. Runtime Failure Modes
15.1 Schema Not Found
Consumer receives schema ID but registry does not have it.
Possible causes:
- wrong registry environment
- replicated topic without schema registry replication
- schema deleted
- network/auth issue
- subject mismatch
Expected handling:
- classify as infrastructure/config failure
- do not silently skip
- alert platform/schema owner
15.2 Deserialization Failure
Possible causes:
- corrupted payload
- wrong serializer/deserializer
- incompatible schema
- wrong topic consumed
- producer bug
Expected handling:
- capture topic/partition/offset/key/schema ID
- route to DLQ if policy allows
- alert if systemic
15.3 Unknown Enum
Expected handling:
- map to UNKNOWN if supported
- preserve raw value if possible
- alert if value indicates incompatible rollout
15.4 Registry Latency/Outage
Consumer may fail if it needs registry lookup at runtime.
Mitigation:
- schema cache
- startup warmup
- bounded timeout
- fallback only if safe
- registry health monitoring
16. Schema Governance and Event Catalog
Schema registry stores schema. Event catalog explains meaning and ownership.
Event catalog should include:
- event name
- business meaning
- producer owner
- consumer inventory
- topic
- key strategy
- schema subject
- compatibility mode
- retention
- replay policy
- deprecation status
- PII/sensitive field classification
- example payload
Without event catalog, schema exists but governance is incomplete.
Schema tells shape. Catalog tells intent.
17. Security and Data Governance
Schema governance must include data sensitivity.
Questions:
- Does event contain PII?
- Is tenant ID sensitive?
- Are price/currency/tax fields confidential?
- Should payload be encrypted?
- Are logs redacting event fields?
- Does DLQ store sensitive payload longer than allowed?
- Does schema registry expose field names that reveal sensitive domain info?
Event retention and DLQ retention must align with data retention policy.
A schema change that adds PII is not just a technical change. It affects logging, retention, access control, and compliance.
18. Date, Time, Currency, and Numeric Compatibility
Event schema must be strict about temporal and numeric fields.
Bad:
{ "price": 10.1 }
Better:
{
"amount": "10.10",
"currency": "USD",
"roundingMode": "HALF_UP"
}
For date/time:
{
"occurredAt": "2026-07-10T04:00:00Z",
"effectiveFrom": "2026-08-01T00:00:00Z"
}
Avoid ambiguous local time unless domain explicitly requires it and includes timezone.
For CPQ/order systems, schema evolution around pricing/catalog effective dates is high risk.
Review:
- decimal precision
- currency code
- rounding policy
- timezone
- effective date semantics
- validity window semantics
19. Schema Version vs Event Version
Do not confuse:
schema version: shape of payload
business event version: semantic version of event contract
aggregate version: state sequence for one aggregate
Example:
{
"eventType": "QuoteApproved",
"eventVersion": 2,
"aggregateVersion": 17
}
Schema registry may assign schema ID/version independently.
Senior review question:
Are we changing syntax, semantics, or aggregate sequence?
A field meaning change might not change schema shape but still requires event version/deprecation plan.
20. Breaking Change Playbook
If breaking change is unavoidable:
- Create new event type or version.
- Keep old event for deprecation window.
- Publish both if necessary.
- Update event catalog.
- Notify consumers.
- Add compatibility matrix.
- Add migration/replay plan.
- Monitor old consumer usage.
- Remove only after approval.
Example:
QuoteApproved v1
approvedBy: string
QuoteApproved v2
approval: { userId, role, channel }
Do not silently change approvedBy meaning.
21. JAX-RS + Event Contract Boundary
JAX-RS DTO and Kafka event schema should not automatically be the same object.
Bad:
public class QuoteApprovalRequest { ... }
producer.send(request);
Problem:
- API request fields are command input, not business fact
- API compatibility and event compatibility differ
- request may contain transient fields not suitable for event
- event needs envelope, aggregate version, causation ID
Better:
HTTP Request DTO
-> command object
-> domain state transition
-> event object/schema
-> Kafka record
Separate contracts:
- OpenAPI for HTTP
- AsyncAPI/event catalog for events
- schema registry for serialization compatibility
22. AsyncAPI and Documentation
AsyncAPI can document:
- topics/channels
- message schema
- headers
- producer/consumer relationship
- examples
- security
- bindings
It complements schema registry.
Schema registry validates payload compatibility. AsyncAPI documents integration contract. Event catalog documents ownership and semantics.
For enterprise governance, all three may exist:
Schema Registry -> technical schema
AsyncAPI -> integration contract
Event Catalog -> ownership and business meaning
23. PR Review Checklist
Schema Format
- Which format is used: Avro, JSON Schema, Protobuf, raw JSON?
- Is schema registered/versioned?
- Is generator version pinned?
- Is schema artifact published?
Compatibility
- What compatibility mode applies?
- Is change backward compatible?
- Is change forward compatible?
- Is transitive compatibility required?
- Are old events replay-tested?
Field Changes
- Are new fields optional/defaulted?
- Are required fields added safely?
- Are fields renamed or removed?
- Are enum values added safely?
- Are numeric/date fields precise and unambiguous?
Event Governance
- Is event owner identified?
- Is event catalog updated?
- Are consumers known?
- Is deprecation policy followed?
- Is replay policy updated?
Security/Data
- Does schema add PII/sensitive data?
- Are logs/DLQ safe?
- Is retention policy affected?
- Is tenant isolation preserved?
Build/Runtime
- Does CI validate compatibility?
- Does runtime handle schema registry outage?
- Are serializer/deserializer configs correct?
- Are DLQ failures observable?
24. Internal Verification Checklist
Untuk codebase/platform internal, verifikasi:
Registry and Format
- Apakah schema registry digunakan?
- Registry vendor/platform apa?
- Schema format: Avro, JSON Schema, Protobuf, custom JSON?
- Subject naming strategy
- Compatibility mode default
- Compatibility override per subject/topic
Build and CI
- Schema stored di repo mana?
- Schema registered via CI atau runtime?
- API lint/schema lint tool
- Generated Java class strategy
- Generated client/server strategy jika ada
- Artifact versioning
Kafka Serialization
- Serializer/deserializer config
- Schema ID location
- Header/content-type convention
- Error handling untuk deserialization failure
- DLQ behavior for bad schema
Event Governance
- Event catalog
- Event owner
- Consumer inventory
- Deprecation process
- Breaking change approval
- Compatibility matrix
Security and Operations
- PII classification in schema
- DLQ retention and access
- Registry authn/authz
- Registry availability/SLO
- Monitoring for schema errors
- Replay policy for old schema
25. Anti-Patterns
Anti-pattern 1 — Raw JSON Without Schema
Producer publishes arbitrary JSON.
Consumer parses with Map<String,Object>.
Dampak:
- no compatibility gate
- field typo becomes runtime bug
- consumer failure discovered late
Anti-pattern 2 — Schema Registry Without Governance
Registry exists but:
- no owner
- no event catalog
- compatibility set to NONE
- schema registered manually
Registry alone is not governance.
Anti-pattern 3 — Reusing Protobuf Field Number
Dampak:
- old payload interpreted as new field
- silent data corruption
Anti-pattern 4 — Adding Required Field Directly
Dampak:
- old producers fail
- old events cannot replay
Anti-pattern 5 — Changing Field Meaning Without Schema Change
Example:
price used to mean pre-tax price
price now means post-tax price
Schema compatibility check passes, business compatibility fails.
Anti-pattern 6 — Event Schema Equals API DTO
Dampak:
- transport command leaks into event fact
- compatibility policies conflict
- missing event metadata
Anti-pattern 7 — DLQ Replay Without Schema Version Awareness
Dampak:
- old payload deserialized with wrong assumptions
- repair creates more inconsistent state
26. Senior Mental Model
Event schema governance is not about choosing Avro vs Protobuf.
It is about controlling change across:
time:
old events, new code, replay
services:
producer, consumer, analytics, operations
protocols:
Kafka, HTTP, gRPC, CDC
data governance:
PII, retention, tenant boundary
release:
rolling deploy, rollback, canary, compatibility gate
A schema that compiles can still be an unsafe contract.
Senior engineer reviews both:
syntactic compatibility
semantic compatibility
27. Summary
Di part ini kita membahas:
- Kafka stores bytes, so schema governance is external
- schema registry concept dan subject naming
- Avro, JSON Schema, dan Protobuf trade-off
- compatibility modes: backward, forward, full, transitive
- schema evolution patterns
- envelope vs payload schema
- producer/consumer compatibility
- compatibility matrix
- generated code governance
- CI/CD schema validation
- runtime schema failure modes
- event catalog, AsyncAPI, and governance
- security, PII, retention, and replay concerns
Part berikutnya membahas event naming, ownership, versioning, deprecation, dan event catalog sebagai lifecycle governance di luar schema shape.
You just completed lesson 74 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.