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

Message Ordering Guarantees

Implement partition keys or sequential IDs to ensure related messages are processed in the correct order.

Ensuring Sequence in Distributed Systems

In this lesson, you will learn how to maintain strict processing order for related messages when using distributed messaging systems in your C# background services.

When you scale background workers, you often shift from a single-threaded loop to multiple instances of "Competing Consumers." While this increases throughput, it introduces a challenge: messages might arrive out of order, or different instances might pick up related messages simultaneously, causing race conditions.

Using Partition Keys

Most message brokers (like Azure Service Bus or Kafka) solve this by using Partition Keys. A partition key is a logical identifier—such as an OrderId or UserId—that you attach to a message. The infrastructure guarantees that all messages sharing the same key are routed to the same partition and processed by the same consumer instance in the exact order they were received.

By tying related work to a specific key, you create an "affinity" that keeps the sequence intact without sacrificing your ability to process unrelated messages in parallel.

Sequential IDs as a Fallback

If your infrastructure doesn't natively support partitioning, or you are managing a local buffer, you must implement Sequential IDs (or version timestamps) within your message contracts. In your consumer, you keep track of the LastProcessedSequenceId for each entity. If a message arrives with an ID that is not LastProcessedSequenceId + 1, you must defer or requeue that message:

public async Task ProcessMessageAsync(Message msg)
{
    var lastId = await _repository.GetLastSequenceId(msg.PartitionKey);
    
    if (msg.SequenceId != lastId + 1)
    {
        // Message arrived out of order, send back to queue to retry later
        await _queue.RequeueAsync(msg);
        return;
    }
    
    // Process and update sequence...
}

This ensures that even if the transport layer delivers messages out of turn, your application logic enforces the logical sequence. Remember that re-queuing introduces latency, so always prioritize broker-level partitioning whenever your infrastructure allows it.

Key Takeaway: Use Partition Keys to bind related work to a single consumer instance, or enforce sequence checks in your consumer logic to guarantee processing order.