Course contentsShow
← C#
Lesson 1152 of 1,90810. Entity Framework Core ModelingFree lesson

Scaling Database Writes

Design architectures to handle high-concurrency write operations using queuing and background workers.

In this lesson, you’ll learn how to offload high-concurrency write operations from your web request thread to a background worker using a message queue.

When your application faces a sudden spike in traffic, hitting your database directly for every single SaveChanges() call can lead to thread exhaustion, locking, and performance degradation. Instead of making the user wait for the database to finish writing, you can decouple the write operation.

The Architecture

Instead of your API writing directly to the database, you publish a "write command" to a message broker (like RabbitMQ, Azure Service Bus, or even an internal Channel<T>). A background service (a BackgroundService or IHostedService) then consumes these messages and performs the actual EF Core write operations in the background.

This approach provides three major benefits:

  1. Throttling: You can control the rate of database writes regardless of incoming request spikes.
  2. Batching: You can group multiple write operations into a single transaction.
  3. Resiliency: If the database is temporarily unreachable, messages stay safely in the queue to be retried automatically.

Implementation Concept

Your API controller becomes a simple producer, while your Worker handles the heavy lifting:

// Inside the Controller: Offload the task
public async Task<IActionResult> CreateOrder(OrderDto dto)
{
    // Serialize the request and push to a queue
    await _queue.Writer.WriteAsync(dto);
    return Accepted(); // Return 202 immediately
}

// Inside a BackgroundService: Process the queue
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await foreach (var order in _queue.Reader.ReadAllAsync(stoppingToken))
    {
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
        
        context.Orders.Add(new Order(order));
        await context.SaveChangesAsync();
    }
}

By moving SaveChangesAsync() to a background worker, your API remains responsive. Since the worker is decoupled, it can process messages at a pace the database can handle, effectively flattening the load and preventing bottlenecks during peak traffic.

Key Takeaway: To scale writes, move high-concurrency operations out of the request-response cycle by queueing commands and processing them asynchronously via background workers.