---
title: 'The Inbox Pattern in .NET (The Outbox''s Missing Half)'
source: 'https://youtube.com/watch?v=vS9_WsNwLlo'
video_id: 'vS9_WsNwLlo'
date: 2026-08-08
duration_sec: 783
---

# The Inbox Pattern in .NET (The Outbox's Missing Half)

> Source: [The Inbox Pattern in .NET (The Outbox's Missing Half)](https://youtube.com/watch?v=vS9_WsNwLlo)

## Summary

This video explains the inbox pattern in .NET, a complementary pattern to the outbox pattern for handling message consumption in distributed systems. The presenter demonstrates how to implement an inbox consumer using MassTransit, PostgreSQL, and RabbitMQ, and discusses the problems it solves, such as duplicate message delivery and ensuring exactly-once side effects.

### Key Points

- **Introduction to the Inbox Pattern** [00:02] — The inbox pattern is introduced as the consumer-side counterpart to the outbox pattern, used to handle message consumption reliably in distributed systems.
- **Why the Inbox Pattern is Needed** [00:16] — Distributed systems have no guarantees; network issues can cause duplicate processing or failed acknowledgments, leading to redelivery. The inbox pattern solves this by introducing an intermediate buffer table in a SQL database.
- **How the Inbox Works** [01:08] — The inbox is the opposite of an outbox. It stores incoming messages with a unique message ID, allowing duplicates to be ignored via a unique constraint. The insert is atomic, so no explicit transaction is needed for just storing the message.
- **Transaction Considerations** [02:00] — If business logic is executed in the same transaction as the inbox insert, problems can arise if the insert succeeds but logic fails, or if side effects occur before commit. The presenter prefers to store the payload only and process it later with a background processor.
- **At-Least-Once Delivery and Exactly-Once Side Effects** [02:43] — Most brokers use at-least-once delivery, so duplicates are possible. The inbox buffers messages, reducing message loss and helping achieve exactly-once side effects.
- **Implementation Setup** [03:10] — The demo app already implements the outbox pattern. It uses MassTransit (free version), PostgreSQL, and RabbitMQ. The inbox implementation parallels the outbox for comparison.
- **Creating the Inbox Message Type** [04:00] — A new folder 'inbox' is created, and the outbox message type is copied and renamed to InboxMessage, with a 'received on UTC' column instead of 'occurred on'.
- **Database Table Setup** [04:27] — The database initializer seeds ~2 million outbox messages and creates tables. An 'inbox messages' table is created with ID, type, content (binary JSON), received on, processed on, and error columns, plus a filtered index for unprocessed messages.
- **Outbox Processor and Publishing** [05:25] — The outbox processor publishes messages to RabbitMQ. The demo is adjusted to generate one message and run every 5 seconds for predictability.
- **Implementing the Inbox Consumer** [05:50] — An InboxConsumer class implements IConsumer<OrderCreatedIntegrationEvent>. MassTransit can make consumers generic, but the presenter falls back to a direct implementation for clarity.
- **Consumer Logic** [07:08] — The consumer injects Npgsql DataSource, opens a connection, and executes an INSERT into the inbox messages table with parameters for ID, type, content, and received time.
- **Handling Duplicates with ON CONFLICT** [08:17] — Using the message ID as primary key, the SQL query uses 'ON CONFLICT (id) DO NOTHING' to silently ignore duplicate inserts, avoiding explicit checks or exception handling.
- **Message ID and Integration Event Base** [09:08] — A base class IntegrationEvent with a MessageID property is created. The message ID from the consume context is used, falling back to a version 7 GUID. The same message ID is used in both inbox and outbox tables.
- **Serialization and Parameters** [10:22] — The message type is obtained from the message itself, content is serialized to JSON, and received time is set to UTC now. The query is updated accordingly.
- **Testing the Consumer** [11:20] — Running the app hits a breakpoint in the consumer, executes the insert, and the inbox table now contains unprocessed messages with the actual payload.
- **Inbox Background Processor** [11:46] — An inbox background service selects unprocessed messages in batches, hands them to a mocked consumer (log statement), and updates the database to mark them processed.
- **Conclusion and Alternatives** [12:23] — The inbox and outbox work together. Maintaining custom implementations is non-trivial; libraries like Wolverine and MassTransit provide built-in support.

### Conclusion

The inbox pattern is essential for reliable message consumption in distributed systems, ensuring exactly-once side effects and preventing message loss. While implementing it manually is non-trivial, libraries like MassTransit and Wolverine offer built-in support.

## Transcript

you're probably going to introduce an outbox on the publisher side. But, what about the consumer side? Well, this is where the inbox pattern comes in, and I'm going to show you how you can implement it in this video, as well as
explain why you would want to use it and which problems it solves. So, the first rule of distributed systems is that nothing is guaranteed, and if you've worked long enough with distributed systems, you probably know the fallacies
of distributed computing by heart. Now, why am I telling you this? Well, this communication here between our message broker and our consumer is a small component of a larger distributed system, but nonetheless, we've got an
external component, the broker, talking to our consumer running inside of our application. And there are multiple ways this can fail. For example, we could run into network issues where the consumer ends up processing the same message
twice. Maybe the consumer completes successfully, but it doesn't succeed in notifying the broker about this, so it may attempt a redelivery. So, the inbox pattern tries to solve this problem by introducing an intermediate buffer table
inside of a SQL database, for example, and we call this the inbox. It's basically the exact opposite of an outbox, and what you do is you open up a atomic guarantees. And after this, you
have a couple of options. You can process the message by taking the message ID and the payload and storing them inside of our inbox table. If we run into a duplicate message ID, which means we got a redelivery from the
broker, then we can just ignore this because we've got a unique constraint on our inbox table. And if we do manage to store this, we can just commit the transaction. Now, when you think about it, this is just one database insert,
which is atomic on its own, so there's even no need to open up an explicit transaction. Now, where our transaction comes into play is if you want to store the message in the inbox table, which is this type here, and also execute the
business logic associated with a specific consumer. Now, this introduces some problems. For example, what if the insert into the inbox succeeds and our to revert the whole thing. And it's even more problematic if the insert initially
succeeds, we execute our business logic, which also produces some side effect, So, there's also a possibility of ending up in an inconsistent state, which is why I like to treat my inbox as just storing the payload within the consumer.
And then I've got a background processor that's going to look for any pending messages in the inbox and go ahead and process them one by one by handing them off to the respective message handler. Also, when you think about it, most
message brokers come with at least once delivery semantics, which means there's always the possibility that you may get the same message delivered more than problem by making sure that your consumers execute exactly once. Although
in practice, we are more concerned with exactly once side effects. But either way, the inbox is going to buffer your incoming messages, so it also reduces the chance of losing any messages. So, that's the high-level introduction. But
how do we actually implement this? So, I've got an application here that's already implementing the outbox pattern, which means that when we want to publish some message, we're going to store it inside of our database table. And then
we've got an outbox processor, which is going to handle any unprocessed outbox messages and hand them off to a queue. For the messaging aspect, I'm using MassTransit, the free and open-source version to be specific. And for my
infrastructure, I'm using Postgres as my database and then RabbitMQ as the message broker. So, why am I starting from an outbox? Well, there are two reasons. The first one is I've already got an example of the outbox, and now I
don't have to implement everything from scratch. And it's It's going to serve as a nice parallel when we want to compare the inbox and the outbox pattern. So, let me create a folder called the inbox. And then, the first thing we would want
is a parallel to the outbox message. So, I can just copy my outbox message into the inbox folder. I'm going to change the name, make sure the type name matches, and I'll also update my name space. Now, instead of occurred on, I
want to call this column received on UTC. So, this is going to represent what we're going to store inside of our inbox table. And that's the next thing I want to talk about. Inside of this database initializer, I've got some logic that's
going to seed my database with around 2 million outbox messages that we're going to process in this demo, but it's also going to initialize my database tables. And in this case, I've got an outbox messages table and a useful filtered
performance of my queries. So, I want to effectively have the same structure for my inbox tables, and it could look something like this. I'm going to create a database table called inbox messages. It's going to have an ID, the type of
the message that we're consuming, the contents in binary JSON format, when it potential errors that happened during processing. I also created a filtered index to help us with finding any unprocessed inbox messages. And this is
going to be a one-for-one match of the contents of my inbox message type. Then, the next thing I want to do is to also truncate this table just to make this demo more reliable. And instead of the outbox here, I'll specify my inbox. The
rest of the code here is going to populate my outbox table. So, when the processor starts, it's going to start publishing these messages to the queue. This happens in the outbox processor class, and this is the specific piece of
code that's going to publish this to RabbitMQ. So, now we want to implement our inbox consumer, and we're going to rely on MassTransit for this. Let me create a type called inbox consumer. I want to use a file scope namespace.
Let's make this internal and sealed and we'll implement the IConsumer interface. Now, what is the message type that we are consuming? Well, in this specific demo, I'm publishing an order created integration event. So, this is what
MassTransit is going to route to my consumer. So, that's what I will also implement in my IConsumer interface. Now, MassTransit is interesting because it can make your consumer generic, which means you could create an inbox consumer
means you could create an inbox consumer of T, make your IConsumer also generic, and you probably need a generic constraint that T is a class. And this may make sense because the logic for every consumer, at least in this
implementation, is going to be to just take the message coming from the broker and store it in the inbox table. Just for the record, to show you how you consumer, specify your inbox consumer, and then
you'd need to define the concrete type. So, in this case, the order created integration event. And MassTransit is actually able to pick this up successfully, and this is going to work at runtime. Now, I do want to comment
this as I want to fall back to the direct version where we're going to handle the order created integration event. So, let me update my consume method and delete my generic parameter here. And to register this with
MassTransit, we'll say add consumer and just inbox consumer. So, what do I want to do here? Well, I'm going to inject my data source from Npgsql and I'm going to use it to open up a database connection. So, let's make this consume method
async. I'll say await using var connection, and we're going to get this using our data source, and I'll call the open connection async method. We can also pass in the cancellation token from our consume context, and then we want to
persist this in the database. And let me define my SQL query that's going to represent this command. So, I want to do an insert into my inbox messages table,
and I'll be inserting the ID, the type, the content, and when this message was received. Then, we're going to specify the values, and I'm going to add these as parameters. So, I'm going to add an at sign in front of them. And because
I'm passing these through using Dapper, I'll just make sure that these are using PascalCase. And because our content is binary JSON, we can also tell our query that to prevent any type mismatch issues. Now, what happens when we
encounter a violation of the unique constraint? We're using our message ID as the uniqueness constraint, which is why we made it our primary key. So, we have an elegant way to solve this in SQL, where I can say on conflict, and I
can specify which columns I want to use for this detection. So, I'll just use the ID column, and I can instruct the database to do nothing and simply ignore this failure. So, whenever we receive the same message multiple times, we're
going to attempt to insert it into the database, essentially sending just one database query, but on any duplicate, we're going to fail silently. And when you think about it, this may make sense. Otherwise, you can explicitly check if
this message exists, or wait for the insert to blow up, and then catch and handle the uniqueness violation exception. Finally, I'll say connection execute async. We're going to pass in our SQL, and we need to pass in our
parameters. For the ID, I'll use the message ID coming from the consume context. Or if this is null, we can create a version 7 message. Another thing I like to do is to have some sort of base class for my integration events.
So, let's say I've got a public abstract record. Let's call it integration event. And on this, I'm going to have a message ID property. So, when I inherit from this, I need to pass in the integration ID value, and by default, we can pass in
a random GUID or even a version 7 GUID. Now, remember that we are serializing this to JSON, so we don't really have to do anything special to get this working with our outbox. And in the inbox, this lets us fall back to the actual message
ID on our integration event. So, now I can use this for my message ID, and you probably want to use the same message ID in both your inbox and your outbox tables. Then, for the message type, I'm going to say type of order created
integration event, and then full name. Of course, we could also get this from our message itself. So, I can say context message get type and then access the full name. Then, for the content, we want to say JSON serializer, serialize,
and then pass in the message. And then, for the receive non-parameter, I'm going to use UTC now, and let me just update my query to make sure this works. So, out. I'm going to update my outbox processor to generate, for example, one
more than enough, and I will update my background service to just not run continuously, and it's going to execute every 5 seconds. This will just make it more predictable to test out our consumer. So, I'm just going to start
execute behind the scenes, and we're going to hit this breakpoint inside of our consumer in a couple of moments. Of course, this assumes that you've got a locally, which I do, and I've got both of them running in Docker containers.
to these. And if we take a look at our database table, you can see that we have an outbox messages table with a bunch of data inside, but we've also got our inbox messages table, which is currently empty. And as I was explaining this,
we're going to hit our breakpoint behind the scenes and execute our SQL query to insert our incoming integration event into the inbox table. So, I'm going to press continue, and then jump into the database. So, now, if I go ahead and
refresh my inbox messages table, you will see that I've got a couple of messages here, and right now they are not processed. However, they contain the actual message in the content column, which we're going to deserialize in our
processor and hand off to the respective message handler. And just for completeness, I'm going to drop in the missing pieces, which are going to be the inbox background service. So, this is going to run continuously behind the
inbox processor, which is very similar to the outbox processor. It's going to select any unprocessed messages in batches from the inbox table and them off to a consumer, which I'm mocking
here with just a log statement, and then update the database to notify it that this batch of inbox messages was processed. So, this is how the inbox and the outbox work together. Now, if you want to see how you can make the
leave a useful article in the description of this video, where I solve, including a bunch of edge cases that aren't so obvious at first look. And if you think this is a lot of code to write, I do agree. It's not trivial
to maintain your own outbox and inbox implementations. So, here's a video where I explain how you can get all of this with a couple of lines of code if you're using Wolverine. MassTransit also supports an inbox and an outbox, as do
most popular messaging libraries. If you enjoyed this video, consider gently tapping the like button to let me know. Thanks a lot for watching, and until Thanks a lot for watching, and until next time, stay awesome.
