Featured image of post The "Chores" Pattern: Offloading Background Tasks

The "Chores" Pattern: Offloading Background Tasks

TLDR: Stop making your users wait for things that don’t matter right now.

I hate waiting. If I click “Save” on a profile page, I want it to save. I don’t want to wait 5 seconds because the backend is busy trying to calculate loyalty points, sync with the ERP, and send a “Welcome” email to my grandmother.

In a production environment, putting everything in the request pipeline is a recipe for disaster. If the ERP is down, the user can’t save their profile? That’s just bad design.

Enter “The Chores” Pattern

We solved this by splitting our responsibilities. The API is for User Interaction. It should be blazing fast. Anything else? That’s a Chore.

Chores are background tasks. They need to happen, but they can happen 5 seconds later, or even—god forbid—at 3 AM.

Real-World Example: Syncing Exchange Rates

In our logistics system, we need fresh exchange rates. But querying an external API every time we generate a quote is slow and expensive.

So, I built a dedicated Azure Function app just for these maintenance tasks. We call it MyProject.Chores.Function (creative, I know).

Here is a simplified look at how we implemented a daily sync using a Timer Trigger:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SyncExchangeRates
{
    private readonly ILogger _logger;
    private readonly IExchangeRateService _exchangeRateService;

    // Dependency Injection works here too!
    public SyncExchangeRates(ILogger logger, IExchangeRateService service)
    {
        _logger = logger;
        _exchangeRateService = service;
    }

    [Function("SyncExchangeRates")]
    // Run at 16:30 every weekday. Why 16:30? Because banks close at 16:00.
    public async Task Run([TimerTrigger("0 30 16 * * 1-5")] TimerInfo myTimer)
    {
        _logger.LogInformation($"[ExchangeRateSync] Running at {DateTime.UtcNow}");

        try
        {
            // The service handles the heavy lifting
            await _exchangeRateService.FetchAndSaveRatesAsync();
            _logger.LogInformation("Rates synced. We are rich!");
        }
        catch (Exception ex)
        {
            // If it fails, our API stays up. The users are happy. I am happy.
            _logger.LogError($"Failed to sync rates: {ex.Message}");
        }
    }
}

Why this wins

  1. Resilience: If the currency provider goes down, my API doesn’t care. We just use yesterday’s rates. It’s better than crashing.
  2. Performance: My API endpoints are not blocked by external HTTP calls.
  3. Peace of mind: I know that “dirty jobs” are handled by the “Chores” function.

We’ve since expanded this to handle everything from cleaning up old logs to generating PDF reports. If it’s not urgent, it’s a Chore.

All rights reserved
Built with Hugo
Theme Stack designed by Jimmy