Agent SkillsAgent Skills
melodic-software

schema-evolution

@melodic-software/schema-evolution
melodic-software
47
8 forks
Updated 4/6/2026
View on GitHub

Schema evolution patterns for backward and forward compatibility

Installation

$npx agent-skills-cli install @melodic-software/schema-evolution
Claude Code
Cursor
Copilot
Codex
Antigravity

Details

Pathplugins/contract-testing/skills/schema-evolution/SKILL.md
Branchmain
Scoped Name@melodic-software/schema-evolution

Usage

After installing, this skill will be available to your AI coding assistant.

Verify installation:

npx agent-skills-cli list

Skill 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:

  1. Invoke docs-management skill for evolution patterns
  2. Verify schema patterns via MCP servers (context7, perplexity)
  3. 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:

  1. Assess Change: Classify as backward/forward/breaking
  2. Choose Strategy: Expand-contract, versioning, or breaking
  3. Implement Carefully: Add defaults, maintain old fields
  4. Test Compatibility: Verify old consumers can read new data
  5. Migrate Gradually: Update consumers before removing old
  6. Document History: Track all schema versions

References

For detailed guidance:


Last Updated: 2025-12-26