Schema evolution patterns for backward and forward compatibility
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: schema-evolution description: Schema evolution patterns for backward and forward compatibility allowed-tools: Read, Glob, Grep, Write, Edit
Schema Evolution Skill
When to Use This Skill
Use this skill when:
- Schema Evolution tasks - Working on schema evolution patterns for backward and forward compatibility
- Planning or design - Need guidance on Schema Evolution approaches
- Best practices - Want to follow established patterns and standards
Overview
Design and manage schema evolution for API and data contracts.
MANDATORY: Documentation-First Approach
Before designing schema evolution:
- Invoke
docs-managementskill for evolution patterns - Verify schema patterns via MCP servers (context7, perplexity)
- Base guidance on schema evolution best practices
Compatibility Dimensions
SCHEMA COMPATIBILITY TYPES:
┌─────────────────────────────────────────────────────────────────┐
│ COMPATIBILITY MATRIX │
├─────────────────────────────────────────────────────────────────┤
│ │
│ BACKWARD COMPATIBLE │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ New schema can read OLD data │ │
│ │ │ │
│ │ Consumer v2 ──reads──► Producer v1 data │ │
│ │ │ │
│ │ Use case: Rolling upgrade where consumers update first │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ FORWARD COMPATIBLE │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Old schema can read NEW data │ │
│ │ │ │
│ │ Consumer v1 ──reads──► Producer v2 data │ │
│ │ │ │
│ │ Use case: Rolling upgrade where producers update first │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ FULL COMPATIBLE │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Both backward AND forward compatible │ │
│ │ │ │
│ │ Consumer v1 ←──reads──► Producer v2 │ │
│ │ Consumer v2 ←──reads──► Producer v1 │ │
│ │ │ │
│ │ Use case: Maximum flexibility, any upgrade order │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ NO COMPATIBILITY (Breaking) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Requires coordinated upgrade │ │
│ │ │ │
│ │ All producers and consumers must update together │ │
│ │ │ │
│ │ Use case: Major version with clean break │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Evolution Rules by Change Type
SCHEMA CHANGE COMPATIBILITY:
┌────────────────────────────┬────────────┬─────────────┬──────────┐
│ Change │ Backward │ Forward │ Full │
├────────────────────────────┼────────────┼─────────────┼──────────┤
│ Add optional field │ ✓ │ ✓ (ignore) │ ✓ │
│ Add required field w/def │ ✓ │ ✗ │ ✗ │
│ Add required field no def │ ✗ │ ✗ │ ✗ │
│ Remove optional field │ ✗ │ ✓ │ ✗ │
│ Remove required field │ ✗ │ ✗ │ ✗ │
│ Rename field │ ✗ │ ✗ │ ✗ │
│ Change field type │ ✗ │ ✗ │ ✗ │
│ Widen type (int→long) │ ✓ │ ✗ │ ✗ │
│ Narrow type (long→int) │ ✗ │ ✓ │ ✗ │
│ Add enum value │ ✓ │ ✗ │ ✗ │
│ Remove enum value │ ✗ │ ✓ │ ✗ │
│ Make field optional │ ✓ │ ✗ │ ✗ │
│ Make field required │ ✗ │ ✓ │ ✗ │
└────────────────────────────┴────────────┴─────────────┴──────────┘
Legend:
✓ = Compatible
✗ = Breaking
Evolution Patterns
Pattern 1: Expand-Contract (Parallel Change)
EXPAND-CONTRACT PATTERN:
Phase 1: EXPAND (Add new alongside old)
┌─────────────────────────────────────────────────────────────────┐
│ Original: { userName: "alice" } │
│ Expanded: { userName: "alice", username: "alice" } │
│ │
│ Producer: writes both fields │
│ Consumer: reads either field (prefers new) │
└─────────────────────────────────────────────────────────────────┘
Phase 2: MIGRATE (Update consumers)
┌─────────────────────────────────────────────────────────────────┐
│ Consumers updated to read: username │
│ Old consumers still work: userName still present │
│ │
│ Monitor: Track usage of old field via logging │
└─────────────────────────────────────────────────────────────────┘
Phase 3: CONTRACT (Remove old field)
┌─────────────────────────────────────────────────────────────────┐
│ Once all consumers migrated: │
│ Final: { username: "alice" } │
│ │
│ Old field removed, migration complete │
└─────────────────────────────────────────────────────────────────┘
Pattern 2: Default Values
// Using default values for backward compatibility
// File: Contracts/OrderDto.cs
public record OrderDto
{
public string Id { get; init; }
public string CustomerId { get; init; }
public List<OrderItemDto> Items { get; init; }
// New field with default for old data
[JsonPropertyName("priority")]
public OrderPriority Priority { get; init; } = OrderPriority.Normal;
// New optional field (null for old data)
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; init; }
// Computed field for backward compatibility
[JsonIgnore]
public decimal Total => Items?.Sum(i => i.Price * i.Quantity) ?? 0;
}
// Deserialization handles missing fields gracefully
public class OrderDtoDeserializer
{
public OrderDto Deserialize(string json)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
return JsonSerializer.Deserialize<OrderDto>(json, options)
?? throw new InvalidOperationException("Failed to deserialize order");
}
}
Pattern 3: Schema Versioning
// Explicit schema versioning in payload
// File: Contracts/VersionedMessage.cs
public interface IVersionedMessage
{
int SchemaVersion { get; }
}
public record OrderCreatedEvent : IVersionedMessage
{
public int SchemaVersion => 2;
public string OrderId { get; init; }
public string CustomerId { get; init; }
public DateTime CreatedAt { get; init; }
// V2 additions
public string? Source { get; init; }
public Dictionary<string, string>? Tags { get; init; }
}
// Version-aware deserializer
public class VersionedDeserializer<T> where T : IVersionedMessage
{
private readonly Dictionary<int, Func<string, T>> _deserializers;
public T Deserialize(string json)
{
// First, peek at version
using var doc = JsonDocument.Parse(json);
var version = doc.RootElement.GetProperty("schemaVersion").GetInt32();
if (_deserializers.TryGetValue(version, out var deserializer))
{
return deserializer(json);
}
// Handle unknown versions
if (version > CurrentVersion)
{
// Forward compatibility: ignore unknown fields
return DeserializeWithLenientOptions(json);
}
throw new SchemaVersionException($"Unsupported schema version: {version}");
}
}
Pattern 4: Union Types / OneOf
// Using discriminated unions for evolution
// File: Contracts/PaymentMethod.cs
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
[JsonDerivedType(typeof(WalletPayment), "wallet")] // Added in v2
public abstract record PaymentMethod
{
public abstract string Type { get; }
}
public record CreditCardPayment : PaymentMethod
{
public override string Type => "credit_card";
public string Last4 { get; init; }
public string Brand { get; init; }
}
public record BankTransferPayment : PaymentMethod
{
public override string Type => "bank_transfer";
public string BankName { get; init; }
public string AccountLast4 { get; init; }
}
public record WalletPayment : PaymentMethod // New type, backward compatible
{
public override string Type => "wallet";
public string WalletProvider { get; init; }
public string WalletId { get; init; }
}
// Consumer handles unknown types gracefully
public class PaymentProcessor
{
public void Process(PaymentMethod payment)
{
switch (payment)
{
case CreditCardPayment cc:
ProcessCreditCard(cc);
break;
case BankTransferPayment bt:
ProcessBankTransfer(bt);
break;
case WalletPayment w:
ProcessWallet(w);
break;
default:
// Forward compatibility: unknown payment type
HandleUnknownPaymentType(payment);
break;
}
}
}
Pattern 5: Optional Wrapper Fields
// Wrapping new structures in optional fields
// File: Contracts/UserProfile.cs
public record UserProfile
{
public string Id { get; init; }
public string Name { get; init; }
public string Email { get; init; }
// V1 address (flat)
[Obsolete("Use StructuredAddress instead")]
public string? Address { get; init; }
// V2 address (structured, optional for backward compat)
public StructuredAddress? StructuredAddress { get; init; }
// Helper for consumers
public string GetFullAddress()
{
if (StructuredAddress != null)
{
return StructuredAddress.Format();
}
return Address ?? string.Empty;
}
}
public record StructuredAddress
{
public string Street { get; init; }
public string City { get; init; }
public string State { get; init; }
public string PostalCode { get; init; }
public string Country { get; init; }
public string Format() =>
$"{Street}, {City}, {State} {PostalCode}, {Country}";
}
Event Sourcing Schema Evolution
EVENT SCHEMA EVOLUTION:
Challenges:
• Events are immutable (stored forever)
• Old events must remain readable
• New code must handle old event formats
Strategies:
1. UPCASTING
Transform old events to new format on read
┌────────────────────────────────────────────────────────────┐
│ Event Store: { type: "OrderCreated_v1", data: {...} } │
│ ↓ │
│ Upcaster: Transform v1 → v2 format │
│ ↓ │
│ Application: Receives OrderCreated_v2 format │
└────────────────────────────────────────────────────────────┘
2. MULTIPLE EVENT HANDLERS
Handle each version explicitly
┌────────────────────────────────────────────────────────────┐
│ Handler: OnOrderCreated_v1(event) { ... } │
│ Handler: OnOrderCreated_v2(event) { ... } │
│ │
│ Event store dispatches to correct handler based on version│
└────────────────────────────────────────────────────────────┘
3. COPY-TRANSFORM (Migration)
Create new events from old
┌────────────────────────────────────────────────────────────┐
│ Migration Job: │
│ 1. Read old events │
│ 2. Transform to new format │
│ 3. Write new events with new type │
│ 4. Mark old events as migrated │
└────────────────────────────────────────────────────────────┘
Message Contract Evolution
// AsyncAPI message evolution example
// File: Contracts/Events/OrderCreatedEvent.cs
/// <summary>
/// Order created event (schema version 3)
///
/// Version history:
/// v1: Initial version (orderId, customerId, createdAt)
/// v2: Added items array
/// v3: Added metadata, source fields
/// </summary>
[AsyncApiMessage("order.created")]
public record OrderCreatedEvent
{
[JsonPropertyName("$schemaVersion")]
public int SchemaVersion => 3;
[JsonPropertyName("orderId")]
[Required]
public string OrderId { get; init; }
[JsonPropertyName("customerId")]
[Required]
public string CustomerId { get; init; }
[JsonPropertyName("createdAt")]
[Required]
public DateTime CreatedAt { get; init; }
// Added in v2
[JsonPropertyName("items")]
public List<OrderItemDto>? Items { get; init; }
// Added in v3
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; init; }
[JsonPropertyName("source")]
public string Source { get; init; } = "unknown";
}
// Upcaster for backward compatibility
public class OrderCreatedEventUpcaster : IEventUpcaster<OrderCreatedEvent>
{
public OrderCreatedEvent Upcast(JsonElement oldEvent, int fromVersion)
{
return fromVersion switch
{
1 => UpcastFromV1(oldEvent),
2 => UpcastFromV2(oldEvent),
3 => JsonSerializer.Deserialize<OrderCreatedEvent>(oldEvent),
_ => throw new UnsupportedSchemaVersionException(fromVersion)
};
}
private OrderCreatedEvent UpcastFromV1(JsonElement oldEvent)
{
return new OrderCreatedEvent
{
OrderId = oldEvent.GetProperty("orderId").GetString()!,
CustomerId = oldEvent.GetProperty("customerId").GetString()!,
CreatedAt = oldEvent.GetProperty("createdAt").GetDateTime(),
Items = null, // Not present in v1
Metadata = null, // Not present in v1
Source = "legacy" // Default for old events
};
}
private OrderCreatedEvent UpcastFromV2(JsonElement oldEvent)
{
var v1Data = UpcastFromV1(oldEvent);
return v1Data with
{
Items = oldEvent.TryGetProperty("items", out var items)
? JsonSerializer.Deserialize<List<OrderItemDto>>(items)
: null
};
}
}
Schema Registry Integration
SCHEMA REGISTRY WORKFLOW:
For Kafka/Event-driven systems:
┌─────────────────────────────────────────────────────────────────┐
│ SCHEMA REGISTRY │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Subject: orders-value │ │
│ │ Schemas: │ │
│ │ v1: { orderId, customerId } │ │
│ │ v2: { orderId, customerId, items[] } │ │
│ │ v3: { orderId, customerId, items[], metadata } │ │
│ │ │ │
│ │ Compatibility: BACKWARD │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ Producer: │
│ 1. Register new schema │
│ 2. Registry checks compatibility │
│ 3. If compatible → assign schema ID │
│ 4. If incompatible → reject registration │
│ │
│ Consumer: │
│ 1. Read message with schema ID │
│ 2. Fetch schema from registry │
│ 3. Deserialize using correct schema │
│ │
└─────────────────────────────────────────────────────────────────┘
Assessment Template
# Schema Evolution Assessment: [API/Event Name]
## Current Schema
- **Name:** [Name]
- **Version:** [Current version]
- **Format:** [JSON/Protobuf/Avro]
- **Compatibility Mode:** [Backward/Forward/Full/None]
## Schema History
| Version | Date | Changes | Compatibility |
|---------|------|---------|---------------|
| [v1] | [Date] | Initial | N/A |
| [v2] | [Date] | [Changes] | [Backward/Breaking] |
## Planned Changes
| Change | Type | Compatibility | Migration Needed |
|--------|------|---------------|------------------|
| [Change] | [Add/Remove/Modify] | [Backward/Breaking] | [Yes/No] |
## Consumers
| Consumer | Current Schema | Support for New | Migration Status |
|----------|----------------|-----------------|------------------|
| [Name] | [Version] | [Yes/No] | [Status] |
## Evolution Strategy
- [ ] Expand-contract pattern applicable
- [ ] Default values defined for new fields
- [ ] Upcasters implemented for old data
- [ ] Schema registry configured
- [ ] Compatibility checks in CI
## Migration Plan
### Phase 1: Expand
- [ ] Add new fields alongside old
- [ ] Deploy producer with both fields
- [ ] Verify backward compatibility
### Phase 2: Migrate
- [ ] Update consumers to use new fields
- [ ] Monitor old field usage
- [ ] Document migration progress
### Phase 3: Contract
- [ ] Remove old fields
- [ ] Update schema documentation
- [ ] Archive old schema versions
## Rollback Plan
[Describe how to rollback if issues occur]
Workflow
When evolving schemas:
- Assess Change: Classify as backward/forward/breaking
- Choose Strategy: Expand-contract, versioning, or breaking
- Implement Carefully: Add defaults, maintain old fields
- Test Compatibility: Verify old consumers can read new data
- Migrate Gradually: Update consumers before removing old
- Document History: Track all schema versions
References
For detailed guidance:
Last Updated: 2025-12-26
More by melodic-software
View allDesign, test, and version prompts with systematic evaluation and optimization strategies.
Evaluate AI systems for fairness using demographic parity, equalized odds, and bias detection techniques with mitigation strategies.
Design human-in-the-loop workflows including review queues, escalation patterns, feedback loops, and quality assurance for AI systems.
Plan ML projects using CRISP-DM, TDSP, and MLOps methodologies with proper phase gates and deliverables.
