10.3.3 – Versioning Rules and Migration Strategies
Module: Section 10 – Kafka Ecosystem & Tools
Section: 10.3 – Schema Evolution & Compatibility
Goal: Master schema versioning best practices and learn how to handle complex schema migrations
Introduction
Schema versioning is more than just incrementing version numbers—it requires strategic planning, coordination, and migration strategies for complex changes.
Why This Module Matters: Poor versioning strategies lead to production outages, data loss, and deployment coordination nightmares. This module teaches you how to evolve schemas safely.
Schema Version Basics
Schema ID vs Schema Version
Subject: "users-value"
Version 1 → Schema ID 5 (First registration for this subject)
Version 2 → Schema ID 12 (Second registration for this subject)
Version 3 → Schema ID 27 (Third registration for this subject)
Key Difference:
- Schema Version: Scoped to subject (1, 2, 3, ...)
- Schema ID: Global across all subjects (never reused, auto-incremented)
Message Format
Every Kafka message includes the global schema ID:
[0x00] [0x00 0x00 0x00 0x0C] [binary_data]
↑ ↑ ↑
Magic Schema ID = 12 Avro-encoded data
Consumer Workflow:
- Extract schema ID (12)
- GET
/schemas/ids/12from Schema Registry - Use schema to deserialize data
Versioning Best Practices
1. Semantic Versioning for Schemas
Apply semver principles:
MAJOR.MINOR.PATCH
1.0.0: Initial schema
1.1.0: Add optional field (backward compatible)
1.2.0: Add another optional field (backward compatible)
1.3.0: Deprecate field (backward compatible)
2.0.0: Remove field or breaking change
Implementation (via doc field):
{
"name": "User",
"namespace": "com.example.v2", // ← Version in namespace
"doc": "User schema v2.0.0",
"fields": [...]
}
2. Use Namespace Versioning
// v1
{"namespace": "com.example.v1", "name": "User"}
// v2 (breaking change)
{"namespace": "com.example.v2", "name": "User"}
Benefits:
- Clear version boundary
- Code generation creates separate classes
- No ambiguity in multi-version systems
3. Document Schema Changes
{
"name": "email",
"type": ["null", "string"],
"default": null,
"doc": "User email address. Added in v1.2.0 (2025-02-01). Required for marketing campaigns."
}
Why?: Future developers (including yourself) need context for schema decisions.
4. Deprecate Before Removing
// v1: Field in use
{"name": "legacyField", "type": "string"}
// v2: Mark deprecated
{
"name": "legacyField",
"type": ["null", "string"],
"default": null,
"doc": "DEPRECATED (v2.0.0): Use newField instead. Will be removed in v3.0.0"
}
// v3: Remove field (6 months later)
// (field gone)
Grace Period: Give consumers time to migrate (e.g., 6-12 months).
5. Test Schema Evolution Locally
# Step 1: Register v1 schema
curl -X POST http://localhost:8081/subjects/users-value/versions \
-d '{"schema": "..."}'
# Step 2: Test v2 compatibility
curl -X POST http://localhost:8081/compatibility/subjects/users-value/versions/latest \
-d '{"schema": "..."}'
# Step 3: If compatible, register v2
curl -X POST http://localhost:8081/subjects/users-value/versions \
-d '{"schema": "..."}'
Handling Breaking Changes
Strategy 1: Dual-Write Migration
Problem: Need to rename field userId → customerId (breaking change).
Solution: Dual-write period.
Phase 1: Add New Field (v2)
{
"fields": [
{"name": "userId", "type": "long"},
{"name": "customerId", "type": "long"} // New field, same value
]
}
Producer Code:
User user = User.newBuilder()
.setUserId(123L)
.setCustomerId(123L) // Duplicate data temporarily
.build();
Duration: 3-6 months (or retention period).
Phase 2: Deprecate Old Field (v3)
{
"fields": [
{
"name": "userId",
"type": ["null", "long"],
"default": null,
"doc": "DEPRECATED: Use customerId"
},
{"name": "customerId", "type": "long"}
]
}
Consumer Code:
// New consumers migrate
long id = user.getCustomerId(); // Use new field
// Old consumers still work
long id = user.getUserId() != null ? user.getUserId() : user.getCustomerId();
Duration: 3-6 months.
Phase 3: Remove Old Field (v4)
{
"fields": [
{"name": "customerId", "type": "long"}
]
}
Validation: Ensure no consumers read userId before removing.
Strategy 2: Topic Migration
Problem: Breaking change that can't be handled via dual-write.
Solution: Create new topic with new schema, migrate data.
Step 1: Create New Topic
kafka-topics.sh --create --topic users-v2 \
--partitions 10 \
--replication-factor 3 \
--config retention.ms=86400000
Step 2: Dual-Write to Both Topics
// Producer writes to both topics
producer.send(new ProducerRecord<>("users", user)); // v1 schema
producer.send(new ProducerRecord<>("users-v2", userV2)); // v2 schema
Duration: Until all consumers migrate.
Step 3: Migrate Consumers
// Consumer switches to new topic
consumer.subscribe(Collections.singletonList("users-v2"));
Step 4: Stop Writing to Old Topic
// Producer only writes to new topic
producer.send(new ProducerRecord<>("users-v2", userV2));
Step 5: Decommission Old Topic
# After retention period expires
kafka-topics.sh --delete --topic users
Strategy 3: Schema Aliasing (Avro Feature)
Problem: Renamed field breaks compatibility.
Solution: Use Avro aliases.
// v1
{"name": "userId", "type": "long"}
// v2 (rename with alias)
{
"name": "customerId",
"type": "long",
"aliases": ["userId"] // ← Maintains backward compatibility
}
Result:
- ✅ v2 consumer can read v1 data (uses alias mapping)
- ✅ v1 consumer can't read v2 data (still breaking forward compatibility)
Use Case: Backward compatibility only (common with consumer-first deployments).
Advanced Migration Patterns
Pattern 1: Expand-Contract (a.k.a. Parallel Change)
Steps:
- Expand: Add new field alongside old field
- Migrate: Consumers switch to new field
- Contract: Remove old field
Timeline:
Week 1: Add customerId (alongside userId)
Week 2-8: Producers write both, consumers read customerId
Week 9: Remove userId
Pattern 2: Strangler Fig Pattern
Use Case: Gradual migration for complex systems.
Steps:
- New service writes to new topic (new schema)
- Old service continues writing to old topic
- Gradually route traffic from old to new service
- Decommission old service
Example:
Old: OrderServiceV1 → orders topic (v1 schema)
New: OrderServiceV2 → orders-v2 topic (v2 schema)
Traffic: 10% → v2, 90% → v1
50% → v2, 50% → v1
100% → v2, 0% → v1
Pattern 3: Blue-Green Schema Deployment
Use Case: Zero-downtime migration for critical topics.
Steps:
- Blue (current): users topic with v1 schema
- Green (new): users-green topic with v2 schema
- Dual-write to both topics
- Switch consumers to green
- Promote green → blue, decommission old blue
Versioning Anti-Patterns
❌ Anti-Pattern 1: Removing Fields Without Deprecation
// v1
{"name": "email", "type": "string"}
// v2 (remove immediately) ← BAD!
// (field gone)
Problem: Consumers crash if they expect email.
Solution: Deprecate for 6-12 months first.
❌ Anti-Pattern 2: Changing Field Type Without Migration Path
// v1
{"name": "age", "type": "int"}
// v2 (change type) ← BAD!
{"name": "age", "type": "string"}
Problem: Incompatible types, consumers can't deserialize.
Solution: Add new field, deprecate old field.
❌ Anti-Pattern 3: No Default Values for New Fields
// v2 (add required field) ← BAD!
{"name": "newField", "type": "string"} // No default!
Problem: v2 consumer can't read v1 data (missing newField).
Solution: Always provide defaults for new fields.
❌ Anti-Pattern 4: Breaking Changes Without Communication
Developer: *Pushes v2 schema with breaking change*
↓
Schema Registry: ❌ Rejected (incompatible)
↓
Developer: *Sets compatibility to NONE*
↓
20 microservices crash in production 🔥
Solution: Breaking changes require cross-team coordination, migration plan, and approval.
Schema Registry Versioning Controls
Temporarily Disable Compatibility
Scenario: Emergency breaking change needed.
# 1. Temporarily disable compatibility
curl -X PUT http://localhost:8081/config/users-value \
-d '{"compatibility": "NONE"}'
# 2. Register breaking schema
curl -X POST http://localhost:8081/subjects/users-value/versions \
-d '{"schema": "..."}'
# 3. Re-enable compatibility
curl -X PUT http://localhost:8081/config/users-value \
-d '{"compatibility": "BACKWARD"}'
⚠️ Use with extreme caution! Requires full coordination with consumers.
Delete Schema Version (Soft Delete)
# Soft delete version 3
curl -X DELETE http://localhost:8081/subjects/users-value/versions/3
Effect: Version marked as deleted, not returned in listings.
Permanent Delete:
curl -X DELETE "http://localhost:8081/subjects/users-value/versions/3?permanent=true"
⚠️ Warning: Permanent delete breaks consumers that have messages with that schema ID!
Migration Checklist
Before Schema Change
- Test compatibility locally
- Check compatibility mode for subject
- Document change in schema
docfield - Notify consuming teams
- Plan rollback strategy
During Migration
- Register new schema via CI/CD (not manually)
- Monitor producer/consumer errors
- Check Schema Registry metrics (registration failures)
- Verify messages with new schema are readable
After Migration
- Update schema documentation
- Remove deprecated code after grace period
- Update schema evolution runbook
- Post-mortem if issues occurred
Key Takeaways
Schema Version: Scoped to subject (1, 2, 3). Schema ID: Global, never reused.
Semantic Versioning: Use MAJOR.MINOR.PATCH for schema versions.
Breaking Changes: Require dual-write, topic migration, or expand-contract pattern.
Dual-Write: Add new field, write both, migrate consumers, remove old field.
Topic Migration: Create new topic, dual-write, migrate consumers, decommission old topic.
Aliases: Avro feature for backward-compatible field renames.
Deprecation: Mark fields deprecated 6-12 months before removing.
Anti-Patterns: No defaults, immediate removal, type changes without migration.
Emergency: Temporarily disable compatibility only with full coordination.
Checklist: Test, document, notify, monitor, rollback plan.
What's Next?
Next Module: 10.3.4 – Real-World Evolution Scenarios
What We Covered:
- ✅ Schema versioning best practices
- ✅ Handling breaking changes
- ✅ Migration strategies
- ✅ Anti-patterns
What's Coming:
- ❌ Real-world evolution examples
- ❌ Complex migration case studies
- ❌ Production lessons learned
References:
- Schema evolution → 10.3.1: Schema Evolution Concepts
- Compatibility modes → 10.3.2: Compatibility Types
- Operations → 10.4: Schema Registry Operations