The Outbox Pattern: Making Sure Your Order Doesn’t Get Lost in the Chaos

Imagine you’re starving, so you order a pizza through your favorite food delivery app. The app confirms your order and everything looks good. But then, the restaurant never gets the message to start making your pizza. Why? The app’s notification system crashed right after saving your order, and the message just… vanished. Now you’re hungry, the restaurant is clueless, and the delivery driver is twiddling their thumbs. Not cool.
This kind of mess is exactly what can happen in a distributed system when messages go missing. Luckily, there’s a clever solution called the Outbox Pattern, and it’s here to make sure your pizza order or any important message gets where it needs to go.
What’s the Outbox Pattern, Anyway?
The Outbox Pattern is a lifesaver for apps that need to send messages reliably. When you place your pizza order, the app saves the order to its database and, at the same time, stores a message about it in a special table called the “outbox.” A background worker—think of it as the app’s personal messenger later picks up that message and sends it to a message broker like RabbitMQ, which then tells the restaurant to fire up the oven.
It’s like texting a friend to pass on a message for you but instead of hoping they don’t forget, the Outbox Pattern makes sure it’s all handled automatically. No lost messages, no hungry customers, just a happy pizza delivery.
Why This Pattern Is a Game-Changer
In a distributed system, you’ve got different parts like the app’s database and the message broker trying to work together. But sometimes they don’t, like when the message broker goes offline or the network hiccups. If you save an order and try to send a message at the same time, you might end up with the order saved but the message lost, leaving the restaurant in the dark.
The Outbox Pattern fixes this by:
Keeping It All Together: It saves everything in the database in one step, so there’s no risk of losing half the job.
Staying Reliable: Messages are stored safely until they’re ready to be sent—no disappearing acts.
Making Sure Everyone Knows: The database and the restaurant stay on the same page.
Basically, it ensures your pizza gets made, even if the app’s notification system decides to take an unexpected nap.
When Should You Use the Outbox Pattern?
This pattern is perfect when:
You can’t risk losing messages (think food delivery, online payments, or medical systems).
You want to avoid complicated setups that might fail if one part goes down.
If you’re building something simple where a lost message isn’t a big deal then you can probably skip this pattern and keep things easy.
How It Works: The Big Picture
1. You place a pizza order, and the app saves both the order and a message in the database together.
2. A background worker checks the outbox table for new messages.
3. It sends the message to RabbitMQ.
4. The “Order Notification Service” picks up the message and notifies the restaurant.
5. Once the restaurant confirms they’ve got it, the service sends a message back to RabbitMQ to confirm everything’s handled.
Let’s Build It: A .NET Implementation That’s Easy to Follow
Let’s create a simple Outbox Pattern setup in .NET to handle our food delivery app’s orders. I’ll break it down into clear steps so you can see how it all fits together.
Step 1: Create the Outbox Message Model
We need a class to store messages in the outbox table.
public class OutboxMessage
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public string EventType { get; set; } // e.g., "OrderPlaced"
public string Data { get; set; } // JSON-serialized payload
public bool IsProcessed { get; set; }
public DateTime? ProcessedAt { get; set; }
}
Step 2: Save the Order and Message in One Transaction
When an order is placed, we save it and the outbox message in one atomic transaction.
public async Task CreateOrderAsync(Order order)
{
var message = new OutboxMessage
{
Id = Guid.NewGuid(),
CreatedAt = DateTime.UtcNow,
EventType = "OrderPlaced",
Data = JsonConvert.SerializeObject(order),
IsProcessed = false
};
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
_dbContext.Orders.Add(order);
_dbContext.OutboxMessages.Add(message);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
Step 3: Build a Background Worker to Process Messages
The worker checks the outbox for unprocessed messages, sends them to RabbitMQ, and marks them as processed.
public class OutboxProcessor : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OutboxProcessor(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var messageSender = scope.ServiceProvider.GetRequiredService<IMessageSender>();
var messages = await dbContext.OutboxMessages
.Where(m => !m.IsProcessed)
.Take(10)
.ToListAsync();
foreach (var message in messages)
{
try
{
await messageSender.SendAsync(message.EventType, message.Data);
message.IsProcessed = true;
message.ProcessedAt = DateTime.UtcNow;
}
catch (Exception ex)
{
Console.WriteLine($"Error sending message {message.Id}: {ex.Message}");
continue;
}
}
await dbContext.SaveChangesAsync();
await Task.Delay(3000, stoppingToken); // Check every 3 seconds
}
}
}
Step 4: Send Messages to RabbitMQ
This is a basic setup for sending messages to RabbitMQ. In a real app, you’d use a library like RabbitMQ.Client
.
public class RabbitMqSender : IMessageSender
{
public async Task SendAsync(string eventType, string data)
{
// Simulate sending to RabbitMQ
Console.WriteLine($"Sent event: {eventType} with data: {data}");
await Task.CompletedTask;
}
}
public interface IMessageSender
{
Task SendAsync(string eventType, string data);
}
Final Thoughts
Now that you’ve got the Outbox Pattern in your toolbox, you’re ready to tackle reliable messaging like a pro. No more worrying about lost messages leaving your customers hungry. Your food delivery app can keep those pizzas rolling out the door, even if part of the system decides to take a break. The beauty of this pattern is its simplicity: by saving messages alongside your orders in the database, you’ve got a safety net that ensures the restaurant always gets the memo.
To make your implementation even more robust, start by setting up some basic monitoring to keep an eye on your outbox table—don’t let unprocessed messages pile up! If RabbitMQ goes down, add a retry mechanism to your background worker so it can try again without missing a beat. And don’t forget to log any hiccups along the way—good logs will be your best friend when troubleshooting. With these tweaks, you’ll have a system that’s not just reliable but also easy to maintain.
So go ahead and put the Outbox Pattern to work in your app. Whether it’s pizzas, burgers, or sushi, you’ll be delivering orders—and customer smiles—with confidence. Happy coding
Subscribe to my newsletter
Read articles from Dayo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Dayo
Dayo
Backend | C# | JavaScript