Course contentsShow
← C#
Lesson 1602 of 1,90814. Background Work and MessagingFree lesson

Version Compatibility Strategies

Support N-1 service versioning for producers and consumers in long-lived systems.

In this lesson, you will learn how to maintain system stability in long-lived asynchronous environments by implementing N-1 version compatibility for your producers and consumers.

When you scale distributed systems, you cannot update every service simultaneously. A message producer might send a message using v2 of a schema while a consumer is still running v1. To avoid breaking your background workers, you must treat your messaging contracts as immutable interfaces that support at least one previous version.

The Strategy: Additive Evolution

Instead of modifying existing message contracts, always add new fields as optional. If you must remove a field, keep it in the code as [Obsolete] for an entire deployment cycle, ensuring the consumer can handle both the presence and absence of that data.

Consider a ProcessOrder message:

// v1 version
public record ProcessOrder(Guid OrderId, decimal Amount);

// v2 version: Keep v1 properties, add optional new fields
public record ProcessOrderV2(
    Guid OrderId, 
    decimal Amount, 
    string? CurrencyCode = "USD" // Default value ensures v1 compatibility
);

Implementing the Consumer Pattern

Your message handlers should be written to be "version-aware." Use a factory or a switch-based dispatcher to handle the incoming payload type. When the broker delivers the message, the consumer must be capable of processing the incoming message type even if the code base has evolved.

In a long-lived system, you can implement an "adapter" pattern where the message consumer checks the version header of the incoming message. If it receives a legacy v1 message, it maps it to the internal v2 domain model before processing:

public async Task HandleAsync(MessageContext context)
{
    var version = context.Headers["Version"];
    var data = version switch
    {
        "1" => MapToV2(JsonSerializer.Deserialize<ProcessOrder>(context.Body)),
        "2" => JsonSerializer.Deserialize<ProcessOrderV2>(context.Body),
        _ => throw new NotSupportedException()
    };
    
    await _processor.ExecuteAsync(data);
}

By decoupling the schema version from the execution logic, you prevent the "Big Bang" deployment trap, allowing your background workers to drain their queues and transition gracefully between versions without dropping messages or failing to deserialize payloads.

Key Takeaway: Always design your message schemas to be additive and use version-aware dispatching in your consumers to ensure that N-1 versions remain functional during rolling deployments.