<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Saga on Krishnatejaswi S</title>
    <link>https://KTS-o7.github.io/bear/tags/saga/</link>
    <description>Recent content in Saga on Krishnatejaswi S</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</managingEditor>
    <webMaster>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</webMaster>
    <copyright>Krishnatejaswi S</copyright>
    <lastBuildDate>Sun, 03 May 2026 00:00:00 +0530</lastBuildDate>
    <atom:link href="https://KTS-o7.github.io/bear/tags/saga/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>The Saga Outbox Pattern: Reliable Event Delivery Without Two-Phase Commit</title>
      <link>https://KTS-o7.github.io/bear/systems/saga_outbox_pattern/</link>
      <pubDate>Sun, 03 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/saga_outbox_pattern/</guid>
      <description>&lt;p&gt;The Saga pattern gives you distributed transactions without cross-service locks. Each step publishes an event when it completes, triggering the next step. But there&amp;rsquo;s a gap in the naive implementation: publishing an event and updating your local database are two separate operations. If your service crashes between them, you get a committed database write with no event published, or an event published with no matching database write. The saga is now in an inconsistent state.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>The Saga pattern gives you distributed transactions without cross-service locks. Each step publishes an event when it completes, triggering the next step. But there&rsquo;s a gap in the naive implementation: publishing an event and updating your local database are two separate operations. If your service crashes between them, you get a committed database write with no event published, or an event published with no matching database write. The saga is now in an inconsistent state.</p>
<p>The outbox pattern closes that gap.</p>
<h2 id="the-dual-write-problem">The Dual-Write Problem</h2>
<p>Say the flight reservation step succeeds. You update your database to mark the reservation as confirmed and then publish a <code>FlightReserved</code> event to your message broker. These are two operations against two different systems. There&rsquo;s no way to make them atomic without 2PC &ndash; which is exactly what sagas are trying to avoid.</p>
<p>If you write to the database first and then crash before publishing, the event is never delivered. The hotel reservation step never starts. The saga stalls. Depending on your monitoring, this might sit undetected for a while.</p>
<p>If you publish first and then crash before writing to the database, you&rsquo;ve told the hotel step to proceed even though the flight reservation isn&rsquo;t durably recorded. The flight service comes back up with no record of the reservation. You&rsquo;ve introduced a phantom.</p>
<h2 id="the-outbox">The Outbox</h2>
<p>The fix: write the event to an outbox table in the same database, in the same local transaction as your state update. Then have a separate process relay events from the outbox to the message broker.</p>





<pre tabindex="0"><code>BEGIN TRANSACTION
  UPDATE flights SET status = &#39;reserved&#39; WHERE id = ?
  INSERT INTO outbox (event_type, payload, created_at)
    VALUES (&#39;FlightReserved&#39;, &#39;{&#34;booking_id&#34;: 123, ...}&#39;, NOW())
COMMIT</code></pre><p>The state change and the event are now atomic with respect to the database. Either both are committed or neither is. The outbox row is durable.</p>
<p>A separate relay process &ndash; sometimes called the outbox processor or message relay &ndash; polls the outbox table, publishes each event to the message broker, and marks the row as processed once delivery is confirmed.</p>





<pre tabindex="0"><code>Relay process:
  SELECT * FROM outbox WHERE processed_at IS NULL ORDER BY created_at LIMIT 100
  FOR EACH row:
    publish to message broker
    UPDATE outbox SET processed_at = NOW() WHERE id = row.id</code></pre><h2 id="at-least-once-delivery">At-Least-Once Delivery</h2>
<p>The relay delivers at-least-once. If the relay crashes after publishing but before marking the row as processed, it will publish the same event again on restart. Downstream consumers have to be idempotent &ndash; processing the same event twice should produce the same result as processing it once.</p>
<p>This is a real design constraint. Your hotel reservation step needs to check whether it already processed a given <code>FlightReserved</code> event before creating a new reservation. Deduplication is typically done with a unique constraint on the event ID or booking ID in the receiving service&rsquo;s database.</p>
<p>At-least-once with idempotent consumers is the standard contract in event-driven systems. Exactly-once is theoretically possible with additional coordination but practically rare &ndash; most systems accept at-least-once and invest in idempotency instead.</p>
<h2 id="change-data-capture-as-an-alternative-relay">Change Data Capture as an Alternative Relay</h2>
<p>Polling the outbox table works but adds load to the database. An alternative is Change Data Capture (CDC): instead of a polling process, you read directly from the database replication log.</p>
<p>Tools like Debezium connect to PostgreSQL&rsquo;s logical replication stream or MySQL&rsquo;s binlog and emit a change event for every row insert. Point Debezium at the outbox table and it publishes events automatically, with no polling query needed.</p>
<p>CDC-based relay has lower overhead and lower latency. The tradeoff: it&rsquo;s another piece of infrastructure to run and operate. For smaller deployments, polling is simpler. For high-throughput systems where polling lag or database load is a concern, CDC is worth it.</p>
<h2 id="saga-state-persistence">Saga State Persistence</h2>
<p>Related to the outbox is the question of where saga state itself lives.</p>
<p>The orchestrator driving the saga needs to know the current step, which steps have completed, and what the compensating actions are if something fails. Two common approaches:</p>
<p><strong>Dedicated saga store</strong>: the orchestrator writes saga state to its own database table after each step. Simple, queryable, easy to inspect when debugging a stuck saga. The outbox pattern applies here too &ndash; saga state update and event publish happen in the same transaction.</p>
<p><strong>Event sourcing</strong>: saga state is derived from the event log itself. There&rsquo;s no explicit state table &ndash; you reconstruct the saga&rsquo;s current position by replaying events. More complex to implement but gives you a full audit trail of everything that happened and an easy path to replaying failed sagas.</p>
<p>Most teams start with a dedicated saga store and move to event sourcing only if they need the audit trail or replay capability.</p>
<h2 id="what-fails-in-compensation">What Fails in Compensation</h2>
<p>One more gap worth naming: compensating transactions can fail too.</p>
<p>You cancel the hotel reservation as part of rolling back a failed booking. The cancel call times out. Now your saga is stuck in a partially compensated state &ndash; the flight is cancelled but the hotel reservation is still live.</p>
<p>The standard answer is retry with backoff. Compensating transactions need to be idempotent for the same reason forward steps do. The outbox pattern helps here too &ndash; compensation requests go through the same outbox mechanism, so they&rsquo;re durably enqueued even if the downstream service is temporarily unavailable.</p>
<p>If compensation keeps failing after retries, the saga is in a &ldquo;saga failure&rdquo; state that requires human intervention or a dead-letter queue. Building an operator UI for stuck sagas is not glamorous work, but it&rsquo;s necessary in any production saga implementation.</p>
<p>The outbox pattern doesn&rsquo;t make sagas simple. It removes the dual-write problem and gives you reliable event delivery &ndash; which turns a fundamentally broken approach into one that&rsquo;s merely complex.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Saga Pattern: Distributed Transactions Without the Lock</title>
      <link>https://KTS-o7.github.io/bear/systems/saga_pattern/</link>
      <pubDate>Sat, 02 May 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/systems/saga_pattern/</guid>
      <description>&lt;p&gt;Two-phase commit gives you distributed atomicity. It also gives you a coordinator that can stall your entire system if it crashes at the wrong moment, and locks held across multiple services for however long the transaction takes. For short, internal transactions that&amp;rsquo;s manageable. For a trip booking flow that touches a flight service, a hotel service, and a payment service &amp;ndash; each owned by a different team, each potentially slow &amp;ndash; it&amp;rsquo;s not.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Two-phase commit gives you distributed atomicity. It also gives you a coordinator that can stall your entire system if it crashes at the wrong moment, and locks held across multiple services for however long the transaction takes. For short, internal transactions that&rsquo;s manageable. For a trip booking flow that touches a flight service, a hotel service, and a payment service &ndash; each owned by a different team, each potentially slow &ndash; it&rsquo;s not.</p>
<p>The Saga pattern is the alternative. You give up true atomicity in exchange for availability, scalability, and no cross-service locks.</p>
<h2 id="how-it-works">How It Works</h2>
<p>Break one big distributed transaction into a sequence of local transactions, each owned by a single service. Each step publishes an event or sends a message when it completes. If any step fails, run compensating transactions in reverse to undo the completed steps.</p>
<p>The classic example is a trip booking:</p>





<pre tabindex="0"><code>Step 1: Reserve flight    → local tx in Flight Service
Step 2: Reserve hotel     → local tx in Hotel Service
Step 3: Charge card       → local tx in Payment Service</code></pre><p>If step 3 fails:</p>





<pre tabindex="0"><code>FORWARD:   [Flight ✓] → [Hotel ✓] → [Payment ✗]
ROLLBACK:              ← [Cancel Hotel] ← [Cancel Flight]</code></pre><p>No global rollback. No coordinator holding locks across all three services. Each service knows how to undo its own step.</p>
<h2 id="two-flavors">Two Flavors</h2>
<p><strong>Choreography</strong> &ndash; each service listens for events and decides what to do next based on what it hears. No central coordinator. Fully decoupled. Easy to start, and a debugging nightmare once you have more than a handful of services. Tracing a failed saga means reconstructing a sequence of events scattered across multiple event streams.</p>
<p><strong>Orchestration</strong> &ndash; a central saga orchestrator drives the sequence. It tells each service what to do, collects results, and issues compensating calls on failure. Full visibility into saga state. Easier to reason about and monitor. The orchestrator becomes a coordination hub &ndash; not quite a single point of failure if you make it durable, but a complexity center. Most teams end up here in production.</p>
<h2 id="the-tradeoff-you-cant-avoid">The Tradeoff You Can&rsquo;t Avoid</h2>
<p>Sagas trade the &ldquo;I&rdquo; in ACID (isolation) for availability. While a saga is in-flight, intermediate state is visible to other parts of the system. A hotel room might be reserved even though the flight hasn&rsquo;t been confirmed yet. Other services can observe that state.</p>
<p>You have to design for this explicitly:</p>
<ul>
<li><strong>Eventual consistency</strong> &ndash; the system converges to a correct final state, but not atomically</li>
<li><strong>Idempotent compensating transactions</strong> &ndash; if your &ldquo;cancel flight&rdquo; call gets retried twice due to a network hiccup, it must be safe to run twice. Compensation that isn&rsquo;t idempotent will make your failure recovery worse than the original failure.</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Property</th>
          <th>2PC</th>
          <th>Saga</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Atomicity</td>
          <td>Global</td>
          <td>Per-step only</td>
      </tr>
      <tr>
          <td>Isolation</td>
          <td>Full</td>
          <td>Intermediate state visible</td>
      </tr>
      <tr>
          <td>Availability</td>
          <td>Blocks on coordinator failure</td>
          <td>Always makes progress</td>
      </tr>
      <tr>
          <td>Scalability</td>
          <td>Locks across services</td>
          <td>No cross-service locks</td>
      </tr>
  </tbody>
</table>
<h2 id="where-it-shows-up">Where It Shows Up</h2>
<p>Basically every large-scale order or booking flow:</p>
<ul>
<li><strong>Uber</strong> &ndash; ride booking, driver assignment, payment charge, all as separate service transactions</li>
<li><strong>Amazon</strong> &ndash; order placement, inventory reservation, fulfillment, shipping</li>
<li><strong>Netflix</strong> &ndash; subscription management, content licensing flows</li>
</ul>
<p>These systems can&rsquo;t afford to hold cross-service locks for the duration of a user-facing transaction. They accept that intermediate states exist and design their compensating logic carefully.</p>
<p>The saga pattern doesn&rsquo;t make distributed transactions easy. It makes the tradeoffs explicit and gives you a way to build systems that stay available even when individual steps fail.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
