Eslam HelmyEslam Helmy
4 min readEslam

Exactly Once Processing: Solving the Outbox Pattern's Double Trouble


Exactly Once Processing: Solving the Outbox Pattern's Double Trouble

So, we faced a critical challenge: we couldn't risk losing messages by communicating directly with the message broker. Our solution? Implementing the outbox pattern. But here's the catch: as with many solutions, this one came with its own set of challenges.

The Double Trouble

Implementing the outbox pattern solved our initial problem, but it introduced a new one: the potential for duplicate message publishing.

Here's how this could happen:

1- A message is successfully added to the outbox table. 2- The message is picked up and sent to the message broker. 3- Before the message status can be updated to "delivered" in the database, a connection issue occurs. 4- The system, unaware that the message was actually sent, will attempt to send it again.

This scenario isn't just a theoretical concern — it's a real-world issue that can occur in distributed systems.

The New Challenge

Now we're faced with a new question: How do we ensure that each message is processed exactly once, even if it's published multiple times? This is where our journey takes us to the next level of message handling reliability: the inbox pattern.

Enter the Inbox Pattern: Our Hero

To tackle the challenge of potential duplicate messages, we turn to the inbox pattern. This clever approach ensures that even if a message is published multiple times, it's processed only once. Here's how it works:

Here's how it works:

1- Each outbox message is assigned a unique MessageId. 2- On the consumer side, we maintain an "inbox" table to keep track of processed messages. 3- Before processing any incoming message, we first check if its MessageId already exists in the inbox. 4- If it's a duplicate, we simply ignore it. 5- If it's new, we process it and record its MessageId in the inbox.

Key Considerations for the Inbox Pattern

While implementing the inbox pattern, keep these important points in mind:

Message Retention: Don't keep processed messages in the inbox table forever. A short retention period (e.g., a few minutes) is usually sufficient and prevents unnecessary database bloat.

Handling Concurrency: To manage concurrent messages with the same request:

Add a unique constraint on the MessageId in the inbox table. When a new message arrives, first attempt to insert its MessageId into the inbox. If insertion fails, it's a duplicate — ignore it. If insertion succeeds, process the message.

This approach effectively prevents race conditions and ensures exactly-once processing.

Continuing Our MassTransit Journey

Since this article is the third in our series discussing MassTransit, we'll build upon our previous knowledge and continue exploring its capabilities. With every consumer, we need to tell MassTransit to use the inbox pattern. To achieve this, we should add the following configuration:

x.AddConfigureEndpointsCallback((context, name, cfg) =>
{
    cfg.UseEntityFrameworkOutbox<AppDbContext>(context);
});

Let's see the full program file of a worker service that has one consumer to imagine where that configuration fits:

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<NotifyReceiptConsumer>();
 
    // Enable the Entity Framework Outbox
    x.AddEntityFrameworkOutbox<AppDbContext>(outboxOptions =>
    {
        outboxOptions.UseSqlServer();
        outboxOptions.DuplicateDetectionWindow = TimeSpan.FromMinutes(2);
    });
 
    // Configure endpoints to enable inbox pattern
    x.AddConfigureEndpointsCallback((context, name, cfg) =>
    {
        cfg.UseEntityFrameworkOutbox<AppDbContext>(context);
    });
    // RabbitMQ configuration...
});

This configuration enables both outbox and inbox patterns for your consumers. Here's what each part does:

1- AddConsumer<NotifyReceiptConsumer>(): Registers your consumer. 2- AddEntityFrameworkOutbox<AppDbContext>(): Sets up the outbox with Entity Framework. 3- AddConfigureEndpointsCallback(): Ensures every endpoint uses the EntityFrameworkOutbox, implementing both outbox and inbox patterns.

With this setup, MassTransit handles the complexities of the outbox and inbox patterns behind the scenes, ensuring your messages are delivered reliably and processed exactly once.

Conclusion: Achieving Message Processing Nirvana

By combining the outbox and inbox patterns, we've created a robust system that guarantees:

  • No messages are lost during publishing.
  • Messages are delivered at least once.
  • Each message is processed exactly once, regardless of how many times it's published.

This approach brings us closer to the holy grail of distributed systems: reliable, consistent, and efficient message processing. While it adds some complexity, the benefits in terms of data integrity and system reliability are well worth the effort.

Remember, in the world of distributed systems, it's not just about sending messages — it's about ensuring they arrive safely and are processed correctly, every single time. Happy messaging!

Share this post