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

Handling Concurrent Event Streams

Manage read-write contention in systems relying on asynchronous event propagation.

In this lesson, you will learn how to resolve data race conditions when multiple asynchronous event handlers attempt to modify the same shared state.

The Conflict: Race Conditions in Event Streams

When working with background services, you often encounter a scenario where multiple incoming events (e.g., OrderUpdated and InventoryAdjusted) trigger concurrent updates to a shared database record or in-memory cache. If two threads read the state, perform a calculation, and write the result back simultaneously, the "lost update" problem occurs. One event’s changes are effectively overwritten by the other because the system lacks a mechanism to serialize these modifications.

Optimistic Concurrency Control

To manage this without locking your entire system, we use Optimistic Concurrency Control (OCC). Instead of preventing access, we detect if the state has changed since we last read it. By adding a Version or RowVersion column to your data model, you can ensure that an update only succeeds if the version hasn't changed.

Consider this logic within your event consumer:

public async Task HandleEventAsync(UpdateEvent @event)
{
    var entity = await _repository.GetByIdAsync(@event.Id);
    
    // Perform business logic
    entity.Value += @event.Adjustment;
    entity.Version++;

    // Only update if the version matches the one we read
    var updated = await _repository.UpdateIfVersionMatchesAsync(entity, @event.OriginalVersion);
    
    if (!updated)
    {
        // Handle collision: re-queue, retry, or move to a Dead Letter Queue
        throw new ConcurrencyException("State changed by another stream; retry needed.");
    }
}

Strategic Ordering

While OCC handles the integrity of the write, you should also consider whether the events themselves need to be sequenced. If your system requires strict chronological order (e.g., "Account Created" must precede "Account Deposited"), routing events with the same PartitionKey (such as an AccountId) to the same consumer instance ensures that they are processed sequentially, significantly reducing the frequency of concurrency collisions.

By combining versioning for atomicity and partitioning for ordering, you create a robust pipeline that maintains consistency even when event streams arrive at high velocity.

Key Takeaway: Use version-based optimistic concurrency for state updates and partition your event streams to minimize contention before it happens.