<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kainat Saricioglu</title>
    <description>The latest articles on DEV Community by Kainat Saricioglu (@kainat_saricioglu).</description>
    <link>https://dev.to/kainat_saricioglu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081992%2F1250d719-e1bf-4072-90d2-2fb3bbd6f870.jpg</url>
      <title>DEV Community: Kainat Saricioglu</title>
      <link>https://dev.to/kainat_saricioglu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kainat_saricioglu"/>
    <language>en</language>
    <item>
      <title>Your Database Said "Success." Your Message Broker Said "Try Again."</title>
      <dc:creator>Kainat Saricioglu</dc:creator>
      <pubDate>Mon, 17 Aug 2026 17:20:39 +0000</pubDate>
      <link>https://dev.to/kainat_saricioglu/your-database-said-success-your-message-broker-said-try-again-4jaj</link>
      <guid>https://dev.to/kainat_saricioglu/your-database-said-success-your-message-broker-said-try-again-4jaj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is where distributed systems get interesting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've worked with backend systems long enough, you've probably written code that looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks reasonable.&lt;/p&gt;

&lt;p&gt;The order is updated.&lt;/p&gt;

&lt;p&gt;The event is published.&lt;/p&gt;

&lt;p&gt;The transaction commits.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Except there is a problem hiding in the middle.&lt;/p&gt;

&lt;p&gt;What happens if &lt;code&gt;PublishAsync()&lt;/code&gt; fails?&lt;/p&gt;

&lt;p&gt;What happens if RabbitMQ is temporarily unavailable?&lt;/p&gt;

&lt;p&gt;What happens if the network connection drops after the broker accepted the message but before your application receives the response?&lt;/p&gt;

&lt;p&gt;What happens if the application crashes at exactly the wrong millisecond?&lt;/p&gt;

&lt;p&gt;And most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does your system believe happened?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is one of those backend problems that doesn't show up in a happy-path demo.&lt;/p&gt;

&lt;p&gt;It shows up at 3:17 AM in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with "just use a transaction"
&lt;/h2&gt;

&lt;p&gt;Let's simplify the system.&lt;/p&gt;

&lt;p&gt;Imagine an Order Service.&lt;/p&gt;

&lt;p&gt;It has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              ┌──────────────┐
              │  Order API   │
              └──────┬───────┘
                     │
                     ▼
              ┌──────────────┐
              │ Order Service│
              └──────┬───────┘
                     │
             ┌───────┴────────┐
             ▼                ▼
      ┌────────────┐   ┌──────────────┐
      │ PostgreSQL │   │   RabbitMQ   │
      └────────────┘   └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request comes in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /orders/123/pay
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service needs to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the database.&lt;/li&gt;
&lt;li&gt;Tell other services that the payment succeeded.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order.Status = Paid
        +
PaymentCompleted event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is where the trouble starts.&lt;/p&gt;

&lt;p&gt;Your database and your message broker are &lt;strong&gt;two different systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A database transaction can guarantee atomicity inside the database.&lt;/p&gt;

&lt;p&gt;RabbitMQ doesn't magically become part of that transaction.&lt;/p&gt;

&lt;p&gt;So this is not really:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN
    UPDATE DATABASE
    PUBLISH MESSAGE
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE                    MESSAGE BROKER

   │                              │
   │ UPDATE                       │
   │─────────────────────────────&amp;gt;│
   │                              │
   │ COMMIT                       │
   │                              │
   │                              │
   │        ??? PUBLISH ???       │
   │                              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a gap.&lt;/p&gt;

&lt;p&gt;And that gap is where distributed systems become difficult.&lt;/p&gt;




&lt;h1&gt;
  
  
  Failure scenario #1: Database first
&lt;/h1&gt;

&lt;p&gt;Let's say we do the sensible-looking thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose the database succeeds.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database
──────────────
Order 123
Status = Paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looks good.&lt;/p&gt;

&lt;p&gt;But immediately afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RabbitMQ
──────────────
PaymentCompleted
       ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe RabbitMQ is temporarily unavailable.&lt;/p&gt;

&lt;p&gt;Maybe DNS failed.&lt;/p&gt;

&lt;p&gt;Maybe the pod restarted.&lt;/p&gt;

&lt;p&gt;Maybe the process crashed.&lt;/p&gt;

&lt;p&gt;Maybe there was a network timeout.&lt;/p&gt;

&lt;p&gt;Now your database says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Payment completed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the rest of your system never heard about it.&lt;/p&gt;

&lt;p&gt;The Inventory Service doesn't know.&lt;/p&gt;

&lt;p&gt;The Notification Service doesn't know.&lt;/p&gt;

&lt;p&gt;The Analytics Service doesn't know.&lt;/p&gt;

&lt;p&gt;Whatever depends on &lt;code&gt;PaymentCompleted&lt;/code&gt; doesn't know.&lt;/p&gt;

&lt;p&gt;And if you simply retry the entire HTTP request, you could create another problem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Failure scenario #2: Message first
&lt;/h1&gt;

&lt;p&gt;So perhaps we reverse the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine the message is successfully published.&lt;/p&gt;

&lt;p&gt;Then the database transaction fails.&lt;/p&gt;

&lt;p&gt;Maybe there is a deadlock.&lt;/p&gt;

&lt;p&gt;Maybe a constraint violation occurs.&lt;/p&gt;

&lt;p&gt;Maybe the database connection disappears.&lt;/p&gt;

&lt;p&gt;Now we have the opposite situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RabbitMQ
──────────────
PaymentCompleted
       ✅

Database
──────────────
Order.Status = Pending
       ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inventory Service receives:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PaymentCompleted&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the Order Service says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actually... no.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we're inconsistent in the other direction.&lt;/p&gt;




&lt;h1&gt;
  
  
  "Can we just use distributed transactions?"
&lt;/h1&gt;

&lt;p&gt;This is where someone usually says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Why not use a distributed transaction?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In theory, we could try to coordinate the database and broker through a distributed transaction protocol.&lt;/p&gt;

&lt;p&gt;In practice, this introduces another set of problems.&lt;/p&gt;

&lt;p&gt;Distributed transactions can be complex, expensive, operationally awkward, and tightly couple infrastructure components.&lt;/p&gt;

&lt;p&gt;The classic transactional outbox pattern exists largely because we want the database update and the intent to publish an event to become atomic &lt;strong&gt;without requiring a two-phase commit across the database and broker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And this is where I think a very simple idea becomes extremely powerful.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Outbox Pattern
&lt;/h1&gt;

&lt;p&gt;Instead of immediately publishing the message, we save the message &lt;strong&gt;inside the same database transaction&lt;/strong&gt; as the business change.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌─────────────────────────┐
                    │       Transaction       │
                    │                         │
                    │  UPDATE Orders          │
                    │          +              │
                    │  INSERT OutboxMessage   │
                    │                         │
                    └────────────┬────────────┘
                                 │
                              COMMIT
                                 │
                    ┌────────────▼────────────┐
                    │      Database            │
                    │                          │
                    │ Orders                   │
                    │ OutboxMessages           │
                    └────────────┬─────────────┘
                                 │
                                 │
                         Background Worker
                                 │
                                 ▼
                         ┌──────────────┐
                         │   RabbitMQ   │
                         └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the important part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application isn't trying to atomically update two systems anymore.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It only has to atomically update one: &lt;strong&gt;the database.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  A simple Outbox table
&lt;/h1&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;OutboxMessages&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Id&lt;/span&gt;              &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;Type&lt;/span&gt;            &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt;         &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OccurredAt&lt;/span&gt;      &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProcessedAt&lt;/span&gt;     &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RetryCount&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our application transaction becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Paid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OutboxMessage&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentCompleted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;OccurredAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutboxMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something important.&lt;/p&gt;

&lt;p&gt;We're &lt;strong&gt;not talking to RabbitMQ inside the transaction anymore&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We're just changing the database.&lt;/p&gt;

&lt;p&gt;Either both changes succeed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders
   +
OutboxMessages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or neither does.&lt;/p&gt;

&lt;p&gt;That's the part we can make truly atomic.&lt;/p&gt;

&lt;p&gt;The transactional outbox pattern specifically works by storing the outgoing message in the same database transaction and having a separate relay publish it to the broker.&lt;/p&gt;




&lt;h1&gt;
  
  
  But now we have another problem
&lt;/h1&gt;

&lt;p&gt;The outbox worker has to read those messages and publish them.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutboxMessages&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessedAt&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OccurredAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks good.&lt;/p&gt;

&lt;p&gt;Until we consider this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Worker reads message
2. Worker publishes message
3. RabbitMQ accepts message
4. Worker crashes
5. Worker never marks message as processed
6. Worker restarts
7. Worker publishes message AGAIN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations.&lt;/p&gt;

&lt;p&gt;We've solved one consistency problem and created another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate messages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And this is one of the most important lessons in distributed systems:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Exactly once" is usually much harder than it sounds.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The outbox relay itself can publish a message more than once if it crashes after publishing but before recording that it was published. The standard pattern therefore expects consumers to be able to process duplicate messages safely.&lt;/p&gt;

&lt;p&gt;Which brings us to my favorite word in distributed systems:&lt;/p&gt;

&lt;h1&gt;
  
  
  Idempotency
&lt;/h1&gt;

&lt;p&gt;An operation is idempotent when performing it multiple times has the same effect as performing it once.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set status = Paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is naturally easier to make idempotent than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;balance += 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set Paid
Set Paid
Set Paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;still results in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Paid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+100
+100
+100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't.&lt;/p&gt;




&lt;h1&gt;
  
  
  Make the consumer idempotent
&lt;/h1&gt;

&lt;p&gt;Suppose our PaymentCompleted event reaches the Notification Service.&lt;/p&gt;

&lt;p&gt;We could create an inbox/processed-message table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ProcessedMessages&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProcessedAt&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;alreadyProcessed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessedMessages&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendPaymentConfirmationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessedMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProcessedMessage&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ProcessedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;MessageId&lt;/code&gt; should also be protected by a database-level unique constraint.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because this is not enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two instances can execute the check concurrently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instance A                 Instance B

    │                          │
    │── Does it exist? ───────&amp;gt;│
    │                          │
    │&amp;lt;────── No ───────────────│
    │                          │
    │                          │
    │── Does it exist? ───────&amp;gt;│
    │                          │
    │&amp;lt;────── No ───────────────│
    │                          │
    ▼                          ▼
  INSERT                     INSERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now both think they're the first.&lt;/p&gt;

&lt;p&gt;That's why the database should enforce the invariant.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ProcessedMessages&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;PK_ProcessedMessages&lt;/span&gt;
&lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL's unique constraints are enforced through unique indexes, making the database itself responsible for preventing duplicate keys.&lt;/p&gt;

&lt;p&gt;This is a pattern I really like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Application logic decides what should happen.&lt;br&gt;
The database enforces what must never happen.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  But wait... there's another race condition
&lt;/h1&gt;

&lt;p&gt;Let's make the system more realistic.&lt;/p&gt;

&lt;p&gt;We have multiple instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌───────────────┐
                    │ Load Balancer │
                    └───────┬───────┘
                            │
                 ┌──────────┼──────────┐
                 ▼          ▼          ▼
             API Pod 1  API Pod 2  API Pod 3
                 │          │          │
                 └──────────┼──────────┘
                            ▼
                        Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine all three workers poll the outbox at the same time.&lt;/p&gt;

&lt;p&gt;They might all see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message #123
ProcessedAt = NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What stops them from all publishing it?&lt;/p&gt;

&lt;p&gt;This is where things get interesting.&lt;/p&gt;

&lt;p&gt;One approach is to atomically claim rows.&lt;/p&gt;

&lt;p&gt;For example, conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;OutboxMessages&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ProcessedAt&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;OccurredAt&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact implementation depends on your database, workload and broker semantics, but the underlying idea is important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading a message and claiming responsibility for it are not necessarily the same operation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And once you start thinking about multiple instances, retries and crashes, the design becomes much more than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's add RabbitMQ."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What about retries?
&lt;/h1&gt;

&lt;p&gt;The worker will fail.&lt;/p&gt;

&lt;p&gt;That's normal.&lt;/p&gt;

&lt;p&gt;A production system should expect failure.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ¯\_(ツ)_/¯&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we need an actual retry strategy.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1
   ↓
failure
   ↓
wait 1 second
   ↓
Attempt 2
   ↓
failure
   ↓
wait 5 seconds
   ↓
Attempt 3
   ↓
failure
   ↓
wait 30 seconds
   ↓
Attempt 4
   ↓
failure
   ↓
Dead Letter / Failed State
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I generally prefer exponential backoff rather than hammering a dependency that is already unhealthy.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But retrying forever is not a strategy.&lt;/p&gt;

&lt;p&gt;Eventually you need to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This message is failing too many times. What do we do with it?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where a dead-letter mechanism, failed state, alerting and operational tooling become important.&lt;/p&gt;

&lt;p&gt;A message stuck for 30 seconds might be normal.&lt;/p&gt;

&lt;p&gt;A message stuck for 30 minutes might be a problem.&lt;/p&gt;

&lt;p&gt;A message stuck for 3 days is probably an incident.&lt;/p&gt;




&lt;h1&gt;
  
  
  Observability is part of the architecture
&lt;/h1&gt;

&lt;p&gt;This is another thing I think backend developers sometimes underestimate.&lt;/p&gt;

&lt;p&gt;Having an outbox is not enough.&lt;/p&gt;

&lt;p&gt;You need to be able to answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How many messages are waiting?

How old is the oldest message?

How many have failed?

How many retries happened?

Which message is failing?

Which service consumed it?

How long did it take?

Are duplicates increasing?

Is the broker unavailable?

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, one metric I'd absolutely want is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;outbox_oldest_message_age_seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue size = 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;doesn't tell me much.&lt;/p&gt;

&lt;p&gt;Those 10 messages might have been created 100 milliseconds ago.&lt;/p&gt;

&lt;p&gt;But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Queue size = 10
Oldest message = 47 minutes old
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is a very different story.&lt;/p&gt;




&lt;h1&gt;
  
  
  There is no "perfect" outbox implementation
&lt;/h1&gt;

&lt;p&gt;And this is probably the most important point.&lt;/p&gt;

&lt;p&gt;The outbox pattern isn't magic.&lt;/p&gt;

&lt;p&gt;It introduces its own costs.&lt;/p&gt;

&lt;p&gt;You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Another table&lt;/li&gt;
&lt;li&gt;Another background process&lt;/li&gt;
&lt;li&gt;Retry logic&lt;/li&gt;
&lt;li&gt;Duplicate handling&lt;/li&gt;
&lt;li&gt;Cleanup/retention&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Potential ordering problems&lt;/li&gt;
&lt;li&gt;Additional database load&lt;/li&gt;
&lt;li&gt;Operational complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The polling-publisher approach, for example, is straightforward and works with SQL databases, but ordering and efficient publishing become concerns. Transaction-log-based approaches can reduce some polling concerns but introduce database-specific infrastructure and their own duplicate-publishing considerations.&lt;/p&gt;

&lt;p&gt;So I don't think the answer should be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Always use the Outbox Pattern."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What consistency guarantee does this particular business operation actually require?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the architectural question.&lt;/p&gt;




&lt;h1&gt;
  
  
  Payment systems make this especially interesting
&lt;/h1&gt;

&lt;p&gt;Imagine this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   │
   ▼
Payment API
   │
   ├──────────────► Payment Provider
   │
   ▼
Database
   │
   ▼
Outbox
   │
   ▼
Message Broker
   │
   ├────────► Order Service
   │
   ├────────► Notification Service
   │
   └────────► Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now think about retries.&lt;/p&gt;

&lt;p&gt;The customer clicks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pay&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The request times out.&lt;/p&gt;

&lt;p&gt;They click again.&lt;/p&gt;

&lt;p&gt;The first payment might have succeeded even though the client never received the response.&lt;/p&gt;

&lt;p&gt;Now you potentially have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP retry
     +
Payment retry
     +
Message retry
     +
Consumer retry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every layer can independently retry.&lt;/p&gt;

&lt;p&gt;And suddenly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency isn't an optimization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a business requirement.&lt;/p&gt;

&lt;p&gt;You might need an idempotency key such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Idempotency-Key: 4f1a7c...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and persist that key with the operation.&lt;/p&gt;

&lt;p&gt;Now your system can distinguish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same request being retried
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one reason I find payment and identity systems particularly interesting: the cost of "doing the same thing twice" can be much higher than simply returning a duplicate record.&lt;/p&gt;




&lt;h1&gt;
  
  
  The architecture I would start with
&lt;/h1&gt;

&lt;p&gt;For a typical .NET microservice, I'd be comfortable starting with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌───────────────┐
                         │     Client    │
                         └───────┬───────┘
                                 │
                                 ▼
                         ┌───────────────┐
                         │ ASP.NET Core  │
                         │      API      │
                         └───────┬───────┘
                                 │
                       ┌─────────▼─────────┐
                       │   DB Transaction  │
                       │                   │
                       │ Business Data     │
                       │        +          │
                       │ Outbox Message    │
                       └─────────┬─────────┘
                                 │
                              COMMIT
                                 │
                                 ▼
                         ┌───────────────┐
                         │ Outbox Worker │
                         └───────┬───────┘
                                 │
                           retry/backoff
                                 │
                                 ▼
                         ┌───────────────┐
                         │   RabbitMQ    │
                         └───────┬───────┘
                                 │
                ┌────────────────┼────────────────┐
                ▼                ▼                ▼
          Order Service    Notification       Analytics
                              Service
                │                │
                └───────┬────────┘
                        ▼
                 Idempotent Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I'd explicitly design for these failure modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database unavailable&lt;/td&gt;
&lt;td&gt;Request fails; nothing is committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business transaction fails&lt;/td&gt;
&lt;td&gt;No event exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broker unavailable&lt;/td&gt;
&lt;td&gt;Event remains in outbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker crashes before publish&lt;/td&gt;
&lt;td&gt;Event is retried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker crashes after publish&lt;/td&gt;
&lt;td&gt;Duplicate is possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer receives duplicate&lt;/td&gt;
&lt;td&gt;Duplicate is safely ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer fails&lt;/td&gt;
&lt;td&gt;Message is retried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message permanently fails&lt;/td&gt;
&lt;td&gt;Dead-letter/failed state + alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple workers process same event&lt;/td&gt;
&lt;td&gt;Database constraint protects invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a much more realistic definition of "reliable" than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works when everything works."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  The part I like most about backend engineering
&lt;/h1&gt;

&lt;p&gt;This is why I still find backend development so interesting.&lt;/p&gt;

&lt;p&gt;The difficult part usually isn't writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difficult part is asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the next line never executes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if it executes but the response is lost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the application crashes immediately afterward?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if two instances do it simultaneously?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the message arrives twice?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the database succeeds but the broker doesn't?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And eventually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the system recover without someone manually fixing the data at 3 AM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where architecture stops being a collection of boxes and arrows.&lt;/p&gt;

&lt;p&gt;It becomes a set of guarantees.&lt;/p&gt;




&lt;h2&gt;
  
  
  My current rule of thumb
&lt;/h2&gt;

&lt;p&gt;When designing distributed backend systems, I try to think in terms of &lt;strong&gt;failure boundaries&lt;/strong&gt;, not just components.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should we use RabbitMQ?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens if RabbitMQ disappears for 10 minutes?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should this be a microservice?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens when this service is unavailable?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should we add retries?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens when the operation succeeds but the response is lost and we retry it?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can we process this asynchronously?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What consistency guarantee does the business actually need?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those questions usually lead to much better architecture decisions.&lt;/p&gt;




&lt;h1&gt;
  
  
  One final thought
&lt;/h1&gt;

&lt;p&gt;I think one of the biggest misconceptions about backend engineering is that reliability comes from adding more infrastructure.&lt;/p&gt;

&lt;p&gt;More services.&lt;/p&gt;

&lt;p&gt;More queues.&lt;/p&gt;

&lt;p&gt;More replicas.&lt;/p&gt;

&lt;p&gt;More caching.&lt;/p&gt;

&lt;p&gt;More Kubernetes.&lt;/p&gt;

&lt;p&gt;More distributed components.&lt;/p&gt;

&lt;p&gt;Sometimes it does.&lt;/p&gt;

&lt;p&gt;But sometimes the best reliability improvement is much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the important invariant explicit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then decide where that invariant should be enforced.&lt;/p&gt;

&lt;p&gt;Sometimes that's the application.&lt;/p&gt;

&lt;p&gt;Sometimes that's the database.&lt;/p&gt;

&lt;p&gt;Sometimes it's both.&lt;/p&gt;

&lt;p&gt;And sometimes the correct answer is to avoid distributing the operation in the first place.&lt;/p&gt;

&lt;p&gt;The more distributed our systems become, the more valuable these fundamentals become.&lt;/p&gt;

&lt;p&gt;Because eventually every distributed system has the same question waiting for us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"What happens when things fail between step A and step B?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the question I'm increasingly interested in.&lt;/p&gt;

&lt;p&gt;And I suspect it's one of the questions that separates code that &lt;em&gt;works&lt;/em&gt; from software that can actually survive production.&lt;/p&gt;




&lt;h3&gt;
  
  
  What would you choose?
&lt;/h3&gt;

&lt;p&gt;Suppose you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database update
      +
Event publication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and you cannot use a distributed transaction.&lt;/p&gt;

&lt;p&gt;Would you choose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A)&lt;/strong&gt; Transactional Outbox&lt;br&gt;
&lt;strong&gt;B)&lt;/strong&gt; Direct publish + retry&lt;br&gt;
&lt;strong&gt;C)&lt;/strong&gt; Event sourcing&lt;br&gt;
&lt;strong&gt;D)&lt;/strong&gt; Something else&lt;/p&gt;

&lt;p&gt;And more importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What failure scenario would drive your decision?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely like to hear how other backend engineers approach this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm planning to write more about the practical side of backend engineering—especially .NET, distributed systems, microservices, databases, identity/security, and the kinds of problems that only become obvious when software meets production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;#BackendDevelopment #DotNet #CSharp #Microservices #DistributedSystems #SoftwareArchitecture #RabbitMQ #PostgreSQL #Database #SystemDesign&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>microservices</category>
      <category>distributedsystems</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
