<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MehradSadeghi]]></title><description><![CDATA[MehradSadeghi]]></description><link>https://mehradsadeghi.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aa6a22601d929592ef631fa/659b2ba6-5552-4916-96e2-a644228bfd8c.png</url><title>MehradSadeghi</title><link>https://mehradsadeghi.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 23 Sep 2026 07:48:51 GMT</lastBuildDate><atom:link href="https://mehradsadeghi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Outbox Pattern Explained: A Complete Guide to Reliable Messaging in Distributed Systems]]></title><description><![CDATA[What happens if your database successfully commits a transaction, but your Message Broker becomes unavailable at exactly the same moment ?
Imagine a user places an order in an online store, and the or]]></description><link>https://mehradsadeghi.hashnode.dev/the-outbox-pattern-explained-a-complete-guide-to-reliable-messaging-in-distributed-systems</link><guid isPermaLink="true">https://mehradsadeghi.hashnode.dev/the-outbox-pattern-explained-a-complete-guide-to-reliable-messaging-in-distributed-systems</guid><category><![CDATA[Microservices]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[outbox pattern]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Mehrad]]></dc:creator><pubDate>Tue, 22 Sep 2026 06:18:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/4ca7a450-5339-4d1d-b2c8-a96c05ac502b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>What happens if your database successfully commits a transaction, but your <a href="https://en.wikipedia.org/wiki/Message_broker">Message Broker</a> becomes unavailable at exactly the same moment ?</p>
<p>Imagine a user places an order in an online store, and the order is successfully stored in the database:</p>
<pre><code class="language-text">Order #123 → Created
</code></pre>
<p>A few milliseconds later, your service tries to send an <code>OrderCreated</code> event to <a href="https://en.wikipedia.org/wiki/Apache_Kafka">Kafka</a> or <a href="https://www.rabbitmq.com/">RabbitMQ</a>, and suddenly:</p>
<ul>
<li><p>The network goes down.</p>
</li>
<li><p>The Broker becomes unavailable.</p>
</li>
<li><p>The application crashes.</p>
</li>
<li><p>A timeout occurs.</p>
</li>
</ul>
<p>So what happens ?</p>
<p>The database says:</p>
<blockquote>
<p>The order has been created.</p>
</blockquote>
<p>But the other services say:</p>
<blockquote>
<p>We have no idea about this order!</p>
</blockquote>
<p>This is one of the classic problems in distributed systems and <a href="https://en.wikipedia.org/wiki/Microservices">Microservices</a> architecture: the <strong>Dual Write Problem</strong>.</p>
<p>This is where the <a href="https://microservices.io/patterns/data/transactional-outbox.html">Outbox Pattern</a> comes in.</p>
<h2>The Core Idea Behind the Outbox Pattern</h2>
<p>The idea behind the Outbox Pattern is surprisingly simple.</p>
<p>Instead of writing to the database and Message Broker at the same time, we store the <strong>business data change</strong> and the corresponding <strong>message/event</strong> in the same <a href="https://dev.to/mehradsadeghi/two-transactions-one-row-what-does-mysql-actually-let-you-see--52ga">database transaction</a>.</p>
<p>We then delegate message publishing to a separate process.</p>
<p>This small change in architecture can make a significant difference to system <a href="https://iamkanikamodi.medium.com/reliability-v-s-resiliency-design-strategies-for-microservices-8d15729da081">reliability</a>.</p>
<h2>What Is the Outbox Pattern ?</h2>
<p>The <strong>Outbox Pattern</strong>, or more precisely the <strong>Transactional Outbox Pattern</strong>, is a design pattern for reliably sending messages and events in distributed systems.</p>
<p>Instead of having the application directly publish an event to a Message Broker after modifying the database, the application first stores the event in an <strong>Outbox Table</strong> in the same database.</p>
<p>The most important point is this:</p>
<blockquote>
<p>The business data change and the event stored in the Outbox must happen within the same database transaction.</p>
</blockquote>
<p>A separate process then reads events from the Outbox and sends them to the Message Broker.</p>
<p>At a high level:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/35b22247-7da8-4200-8411-82cab0db8c9e.png" alt="" style="display:block;margin:0 auto" />

<h2>Why Do We Need the Outbox Pattern ?</h2>
<p>To understand the Outbox Pattern, we first need to understand the problem it solves.</p>
<p>Suppose we have an <code>Order Service</code> that needs to perform two operations:</p>
<ol>
<li><p>Store the order in the database.</p>
</li>
<li><p>Send the <code>OrderCreated</code> event to Kafka.</p>
</li>
</ol>
<p>Our code might initially look like this:</p>
<pre><code class="language-text">BEGIN TRANSACTION
INSERT INTO orders (...)
COMMIT
</code></pre>
<pre><code class="language-text">publish(OrderCreated)
</code></pre>
<p>At first glance, this seems reasonable.</p>
<p>But there is a dangerous gap between <code>COMMIT</code> and <code>publish()</code>.</p>
<p>What happens if the database commits successfully but the application crashes before publishing the event ?</p>
<pre><code class="language-text">Database
   │
   └── Order Created ✅

Message Broker
   │
   └── OrderCreated ❌
</code></pre>
<p>Now the system is in an inconsistent state:</p>
<ul>
<li><p>The internal business state has changed.</p>
</li>
<li><p>The corresponding event was never published.</p>
</li>
</ul>
<p><a href="https://aws.amazon.com/">AWS</a> also describes Transactional Outbox as a solution to the Dual Write problem: a situation where one logical operation needs to modify both a database and a messaging system, while failure in one of them can leave the systems inconsistent.</p>
<h2>What Is the Dual Write Problem ?</h2>
<p>A <strong>Dual Write</strong> occurs when one logical operation needs to be writtern into two independent systems.</p>
<p>For example:</p>
<pre><code class="language-text">Database
    +
Kafka
</code></pre>
<p>Or:</p>
<pre><code class="language-text">Database
    +
RabbitMQ
</code></pre>
<p>Or even:</p>
<pre><code class="language-text">Database
    +
External API
</code></pre>
<p>The problem is that we usually don't have a shared transaction between these two systems.</p>
<p>For example:</p>
<pre><code class="language-text">1. UPDATE database ✅
2. SEND message    ❌
</code></pre>
<p>Or the opposite:</p>
<pre><code class="language-text">1. SEND message    ✅
2. UPDATE database ❌
</code></pre>
<p>In the first case, the event is lost.</p>
<p>In the second case, an event has been published even though the corresponding business state does not actually exist.</p>
<h2>Why Isn't a Normal Database Transaction Enough ?</h2>
<p>You might say:</p>
<blockquote>
<p>Why don't we simply put message publishing inside the transaction ?</p>
</blockquote>
<p>The problem is that the database and Message Broker are two independent resources. To make the entire operation <a href="https://en.wikipedia.org/wiki/ACID">atomic</a>, we would need some form of <a href="https://en.wikipedia.org/wiki/Distributed_transaction">distributed transaction</a>.</p>
<p>One classic approach is <strong>Two-Phase Commit</strong>, or <strong>2PC</strong>.</p>
<p>In 2PC, a coordinator asks all participants:</p>
<blockquote>
<p>Are you ready to commit your changes ?</p>
</blockquote>
<p>Only when all participants agree, the coordinator instruct them to perform the final commit.</p>
<p>If one participant has a problem, the entire operation can be stopped.</p>
<p>This approach is theoretically attractive, but modern distributed architectures can introduce problems such as:</p>
<ul>
<li><p>Complexity</p>
</li>
<li><p>Latency</p>
</li>
<li><p>Coupling</p>
</li>
<li><p>Failure handling</p>
</li>
</ul>
<p>In addition, not every database or Message Broker supports distributed transactions in the same way.</p>
<p>The Outbox Pattern asks a different question:</p>
<blockquote>
<p>Do we really need the database and Broker to participate in one transaction ?</p>
</blockquote>
<p>The answer is:</p>
<blockquote>
<p>No.</p>
</blockquote>
<p>Instead, we can reliably record the critical operation in a local database transaction and defer message publishing until afterward.</p>
<h2>How Does the Outbox Pattern Solve the Dual Write Problem ?</h2>
<p>Instead of:</p>
<pre><code class="language-text">Database → Commit
Broker   → Publish
</code></pre>
<p>we use:</p>
<pre><code class="language-text">Database
├── Business Data
└── Outbox Event
</code></pre>
<p>For example:</p>
<pre><code class="language-sql">BEGIN;
INSERT INTO orders (id, customer_id, status) VALUES (123, 456, 'CREATED');
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload) VALUES ('event-789', 'Order', 123, 'OrderCreated', '{...}');
COMMIT;
</code></pre>
<p>If the transaction succeeds:</p>
<pre><code class="language-text">Order        ✅
Outbox Event ✅
</code></pre>
<p>If the transaction fails:</p>
<pre><code class="language-text">Order        ❌
Outbox Event ❌
</code></pre>
<p>The database guarantees that both changes are committed or rolled back together.</p>
<p>This is the key idea behind the Outbox Pattern.</p>
<h2>What Is an Outbox Table ?</h2>
<p>An Outbox is usually just a normal database table.</p>
<p>For example:</p>
<pre><code class="language-sql">CREATE TABLE outbox (
    id              UUID PRIMARY KEY,
    aggregate_type  VARCHAR(100),
    aggregate_id    VARCHAR(100),
    event_type      VARCHAR(200),
    payload         JSONB,
    created_at      TIMESTAMP,
    processed_at    TIMESTAMP NULL
);
</code></pre>
<p>The actual schema can vary depending on your system's requirements.</p>
<p>Common fields include:</p>
<h3>1. <code>id</code></h3>
<p>A unique identifier for the event.</p>
<p>This ID is extremely important for <a href="https://en.wikipedia.org/wiki/Data_deduplication">Deduplication</a> and <a href="https://medium.com/@jyc.dev/idempotency-in-software-engineering-why-it-matters-and-how-to-implement-it-2025-guide-c1ef8ad21965">Idempotency</a>.</p>
<h3>2. <code>aggregate_id</code></h3>
<p>For example:</p>
<pre><code class="language-text">order_id = 123
</code></pre>
<p>This field can be important for maintaining event ordering in many architectures.</p>
<h3>3. <code>event_type</code></h3>
<p>For example:</p>
<pre><code class="language-text">OrderCreated
OrderPaid
OrderCancelled
</code></pre>
<h3>4. <code>payload</code></h3>
<p>The actual event data:</p>
<pre><code class="language-json">{
  "orderId": 123,
  "customerId": 456,
  "totalAmount": 250
}
</code></pre>
<h3>5. <code>created_at</code></h3>
<p>The time at which the event was created.</p>
<h3>6. <code>processed_at</code></h3>
<p>With a polling-based implementation, this field can be used to identify events that have already been processed.</p>
<h2>How Does the Complete Outbox Flow Work ?</h2>
<p>Let's look at the complete flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/3df24f20-7c97-40fc-bb7d-7dca543ac234.png" alt="" style="display:block;margin:0 auto" />

<p>Step by step:</p>
<ol>
<li><p>The Client sends a request to create an Order.</p>
</li>
<li><p>The Order Service starts a Transaction.</p>
</li>
<li><p>The Order is stored in the database.</p>
</li>
<li><p>The corresponding Event is stored in the Outbox.</p>
</li>
<li><p>The Transaction is committed.</p>
</li>
<li><p>An Outbox Publisher reads the event from the Outbox.</p>
</li>
<li><p>The Event is sent to the Message Broker.</p>
</li>
<li><p>Different Consumers receive the Event.</p>
</li>
<li><p>After successful publishing, the Outbox Event is cleaned up or marked as processed.</p>
</li>
</ol>
<p>An important detail is that the application does not need to wait for the Broker for the original request to succeed.</p>
<p>This means event publishing can happen asynchronously.</p>
<h2>A Real-World Example: Creating an Order</h2>
<p>Suppose a user creates order <code>1001</code>.</p>
<h3>Step 1: Create the Order</h3>
<pre><code class="language-http">POST /orders
</code></pre>
<p>The application starts a transaction.</p>
<h3>Step 2: Store the Order</h3>
<pre><code class="language-text">orders

id: 1001
status: CREATED
</code></pre>
<h3>Step 3: Store the Event</h3>
<p>Inside the same transaction:</p>
<pre><code class="language-text">outbox

id: event-abc
event_type: OrderCreated
aggregate_id: 1001
</code></pre>
<h3>Step 4: Commit</h3>
<p>If the commit succeeds:</p>
<pre><code class="language-text">Order  → persisted
Event  → persisted
</code></pre>
<p>Now, even if the application crashes immediately afterward, the event has not been lost.</p>
<h3>Step 5: Publish the Event</h3>
<p>The Publisher eventually sends:</p>
<pre><code class="language-text">Kafka → OrderCreated
</code></pre>
<h3>Step 6: Consumers Process the Event</h3>
<p>For example:</p>
<pre><code class="language-text">Inventory Service:

OrderCreated
      ↓
Reserve Stock
</code></pre>
<pre><code class="language-text">Notification Service:

OrderCreated
      ↓
Send Confirmation Email
</code></pre>
<pre><code class="language-text">Analytics Service:

OrderCreated
      ↓
Record Conversion
</code></pre>
<p>This is where the Outbox Pattern becomes especially useful for <a href="https://en.wikipedia.org/wiki/Event-driven_architecture">Event-Driven Architecture</a>.</p>
<h2>Does the Outbox Pattern Prevent Duplicate Messages ?</h2>
<p><strong>No.</strong></p>
<p>This is one of the most important things to understand about the Outbox Pattern.</p>
<p>The Outbox Pattern can ensure that an event is not lost, but it does not necessarily guarantee that the event will be published exactly once.</p>
<p>Consider this sequence:</p>
<ol>
<li><p>The Publisher reads the event.</p>
</li>
<li><p>The Publisher sends the event to Kafka.</p>
</li>
<li><p>Kafka successfully receives the message.</p>
</li>
<li><p>The Publisher crashes before marking the event as processed.</p>
</li>
</ol>
<p>After the Publisher restarts:</p>
<pre><code class="language-text">Publisher
   ↓
"This event is still unprocessed"
   ↓
Publish Again
</code></pre>
<p>As a result, <code>OrderCreated</code> may reach the Consumer twice.</p>
<p>This behavior is normal and is associated with <a href="https://medium.com/@madhur25/meaning-of-at-least-once-at-most-once-and-exactly-once-delivery-10e477fafe16">At-Least-Once Delivery</a>.</p>
<p><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html">AWS's Transactional Outbox guidance</a> also discusses duplicate messages and the need for idempotent Consumers.</p>
<h2>Idempotency: The Other Half of the Story</h2>
<p>If you use the Outbox Pattern, you need to ask:</p>
<blockquote>
<p>What happens if the same event reaches a Consumer twice ?</p>
</blockquote>
<p>For example:</p>
<pre><code class="language-text">PaymentCompleted
</code></pre>
<p>Suppose the Consumer processes the event twice and each time performs:</p>
<pre><code class="language-text">$100 → Charge Customer
</code></pre>
<p>The customer could potentially be charged twice.</p>
<p>The Consumer therefore needs a way to detect duplicate events.</p>
<p>One common approach is to maintain a table such as:</p>
<pre><code class="language-text">processed_events

event_id
--------
event-123
event-456
</code></pre>
<p>Before processing:</p>
<pre><code class="language-text">Does event-123 exist ?
</code></pre>
<p>If it exists:</p>
<pre><code class="language-text">Ignore
</code></pre>
<p>If it doesn't:</p>
<pre><code class="language-text">Process
Insert event_id
</code></pre>
<p>Ideally, these operations should also happen inside a transaction.</p>
<p>This makes the Outbox Pattern and the Idempotent Consumer Pattern complementary solutions.</p>
<h2>Does Outbox Mean Exactly-Once Delivery ?</h2>
<p>This is where terminology matters.</p>
<p>You sometimes hear:</p>
<blockquote>
<p>The Outbox Pattern provides Exactly-Once Delivery.</p>
</blockquote>
<p>That statement is an oversimplification.</p>
<p>The Outbox Pattern by itself does not provide an exactly-once guarantee.</p>
<p>A more realistic architecture looks like this:</p>
<pre><code class="language-text">Producer
   ↓
Outbox
   ↓
At-Least-Once Publish
   ↓
Idempotent Consumer
</code></pre>
<p>This combination can provide much more reliable behavior in terms of the final business state.</p>
<p>However, if the Consumer calls an external API, guaranteeing exactly-once behavior across the entire chain becomes much more complicated.</p>
<p>The important point is:</p>
<blockquote>
<p>Exactly-once is not simply a property of one component; it is an end-to-end property.</p>
</blockquote>
<h2>How Is an Outbox Event Published to the Broker ?</h2>
<p>There are two common approaches:</p>
<ol>
<li><p><a href="https://medium.com/@nustianrwp/the-transactional-outbox-pattern-a-rigorous-examination-for-distributed-systems-engineers-9c189836f470">Polling</a></p>
</li>
<li><p><a href="https://medium.com/@nustianrwp/the-transactional-outbox-pattern-a-rigorous-examination-for-distributed-systems-engineers-9c189836f470">Change Data Capture (CDC)</a></p>
</li>
</ol>
<h3>Approach 1: Polling Publisher</h3>
<p>With polling, a worker periodically queries the Outbox table.</p>
<p>For example:</p>
<pre><code class="language-sql">SELECT * FROM outbox WHERE processed_at IS NULL ORDER BY created_at LIMIT 100;
</code></pre>
<p>The workflow is:</p>
<pre><code class="language-text">Read
 ↓
Publish
 ↓
Mark as processed
</code></pre>
<p>The biggest advantage of polling is simplicity.</p>
<p>You don't need your system to work directly with the database's Transaction Log. Almost any database that supports queries can be used to build this type of architecture.</p>
<p>However, polling has a latency trade-off.</p>
<p>For example, if you poll every five seconds, an event created immediately after a polling cycle might have to wait almost five seconds before being detected.</p>
<p>If you poll more frequently:</p>
<pre><code class="language-text">100ms
50ms
10ms
</code></pre>
<p>you increase the load on the database.</p>
<p>So you have a trade-off:</p>
<pre><code class="language-text">Polling Frequency
       ↕
    Latency
       ↕
Database Load
</code></pre>
<hr />
<h3>Approach 2: Change Data Capture (CDC)</h3>
<p>With <a href="https://medium.com/@nustianrwp/the-transactional-outbox-pattern-a-rigorous-examination-for-distributed-systems-engineers-9c189836f470">Change Data Capture (CDC)</a>, instead of repeatedly querying the Outbox table, we track changes in the database's Transaction Log.</p>
<p>For example:</p>
<pre><code class="language-text">Application
     ↓
Database
     ↓
Transaction Log
     ↓
CDC Connector
     ↓
Message Broker
</code></pre>
<p>Different databases may use transaction logs such as:</p>
<pre><code class="language-text">WAL
Binlog
Redo Log
</code></pre>
<p>Tools such as <a href="https://debezium.io/">Debezium</a> can capture changes to an Outbox Table from the Transaction Log and transform them into events.</p>
<p>Debezium also provides a dedicated <a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html">Outbox Event Router</a> for this scenario.</p>
<h3>Advantages of CDC</h3>
<ul>
<li><p>Lower latency</p>
</li>
<li><p>Less continuous polling</p>
</li>
<li><p>Suitable for high throughput</p>
</li>
<li><p>Better alignment with Transaction Log changes</p>
</li>
</ul>
<h3>Disadvantages of CDC</h3>
<ul>
<li><p>More architectural complexity</p>
</li>
<li><p>More infrastructure and monitoring requirements</p>
</li>
<li><p>Dependency on database capabilities</p>
</li>
<li><p>Higher operational complexity</p>
</li>
</ul>
<p>Therefore, CDC is not necessarily better than polling. It is simply more appropriate for certain scales and latency requirements.</p>
<h2>Polling or CDC: Which Should You Choose ?</h2>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/wo4hlanfin0xd84emsfe.png" alt="Image description" style="display:block;margin:0 auto" />

<p>If your system is still relatively new, polling can be a straightforward approach.</p>
<p>If event volume is high and low latency is particularly important, CDC can become a more suitable option.</p>
<p>The choice ultimately depends on factors such as:</p>
<ul>
<li><p>Event volume</p>
</li>
<li><p>Latency requirements</p>
</li>
<li><p>Database capabilities</p>
</li>
<li><p>Operational complexity</p>
</li>
<li><p>Infrastructure maturity</p>
</li>
</ul>
<h2>Does the Outbox Pattern Preserve Event Ordering ?</h2>
<p>Not automatically, and not in every architecture.</p>
<p>Suppose we have:</p>
<pre><code class="language-text">OrderCreated
OrderPaid
OrderShipped
</code></pre>
<p>A Consumer may need to receive these events in exactly this order.</p>
<p>If it receives:</p>
<pre><code class="language-text">OrderShipped
OrderCreated
OrderPaid
</code></pre>
<p>the business logic could behave incorrectly.</p>
<p>Therefore, ordering needs to be designed intentionally.</p>
<p>For example:</p>
<pre><code class="language-text">aggregate_id = order-123
</code></pre>
<p>You can then route events belonging to the same Aggregate to the same Partition in the Message Broker.</p>
<p>With Kafka, a Partition Key can help ensure that events for a particular Aggregate are placed in the same Partition and processed in order within that Partition.</p>
<p>The important distinction is:</p>
<blockquote>
<p>The Outbox Pattern and Message Ordering are related, but they are separate problems.</p>
</blockquote>
<h2>What Happens to the Outbox Table Over Time ?</h2>
<p>Suppose your system generates:</p>
<pre><code class="language-text">1,000 events / second
</code></pre>
<p>That's approximately:</p>
<pre><code class="language-text">86,400,000 events / day
</code></pre>
<p>If you never delete or archive processed events, the Outbox table will grow rapidly.</p>
<p>This can lead to:</p>
<ul>
<li><p>Increased database size</p>
</li>
<li><p>Larger <a href="https://en.wikipedia.org/wiki/Database_index">database indexes</a></p>
</li>
<li><p>Heavier backups</p>
</li>
<li><p>Slower queries</p>
</li>
<li><p>Higher storage costs</p>
</li>
</ul>
<p>Therefore, an Outbox needs a <strong>Lifecycle Strategy</strong>.</p>
<p>For example:</p>
<pre><code class="language-text">Pending
   ↓
Published
   ↓
Retention Period
   ↓
Delete / Archive
</code></pre>
<h3>Three Common Strategies</h3>
<h4>1. Delete</h4>
<p>Delete events after they have been successfully published.</p>
<p>This is simple and inexpensive, but it makes historical replay more difficult.</p>
<h4>2. Archive</h4>
<p>Move events to another storage system after a certain period.</p>
<p>This can be more useful for:</p>
<ul>
<li><p>Auditing</p>
</li>
<li><p>Debugging</p>
</li>
<li><p>Historical analysis</p>
</li>
</ul>
<h4>3. Partitioning</h4>
<p>Partition the table based on time.</p>
<p>For example:</p>
<pre><code class="language-text">outbox_2026_09_18
outbox_2026_09_19
outbox_2026_09_20
</code></pre>
<p>Old partitions can then be deleted or archived efficiently.</p>
<h2>Advantages of the Outbox Pattern</h2>
<h3>1. Preventing Lost Events</h3>
<p>The most important advantage of the Outbox Pattern is that when the Business Transaction commits, the Event is also stored.</p>
<p>Therefore, if the Publisher crashes immediately after the commit, the Event is not lost.</p>
<h3>2. No Need for 2PC</h3>
<p>The database and Broker do not need to participate in a shared Distributed Transaction.</p>
<h3>3. Separation of Business Operations and Message Publishing</h3>
<p>The Business Request does not need to wait for the Broker.</p>
<h3>4. Suitable for Event-Driven Architecture</h3>
<p>The Outbox Pattern is particularly useful for communication between Microservices.</p>
<h3>5. Easier Retries</h3>
<p>If the Broker is temporarily unavailable:</p>
<pre><code class="language-text">Outbox
   ↓
Retry
   ↓
Retry
   ↓
Success
</code></pre>
<p>The Event remains safely stored in the database.</p>
<h2>Disadvantages of the Outbox Pattern</h2>
<p>The Outbox Pattern solves one class of reliability problems, but it also introduces additional responsibilities.</p>
<h3>1. Increased Complexity</h3>
<p>Instead of having only a database, you may now have:</p>
<pre><code class="language-text">Database
+
Outbox
+
Publisher
+
Broker
+
Retry
+
Monitoring
+
Cleanup
</code></pre>
<h3>2. Duplicate Events</h3>
<p>The Publisher may send an Event more than once.</p>
<p>Therefore, Consumers should be designed to be idempotent.</p>
<h3>3. Eventual Consistency</h3>
<p>An Event does not necessarily reach every service immediately after the database transaction commits.</p>
<p>For a short period, you might have:</p>
<pre><code class="language-text">Order Service     → CREATED
Inventory Service → still knows nothing
</code></pre>
<p>This delay needs to be acceptable from a business perspective.</p>
<h3>4. Outbox Growth</h3>
<p>Without a proper lifecycle strategy, the Outbox can become a very large table.</p>
<h3>5. New Failure Modes</h3>
<p>You now need to handle problems such as:</p>
<pre><code class="language-text">Publisher Crash
Broker Down
Database Down
Poison Message
Retry Storm
Duplicate Event
Outbox Backlog
</code></pre>
<p>The Outbox Pattern therefore does not eliminate reliability problems.</p>
<p>Instead, it changes the types of failures your system needs to handle.</p>
<h2>Outbox Pattern vs. 2PC</h2>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ebkd170fxuehxonpx2le.png" alt="Image description" style="display:block;margin:0 auto" />

<p>The Outbox Pattern takes a fundamentally different approach from distributed transactions.</p>
<p>Instead of trying to create one global transaction, it keeps the transaction within the boundaries of a single database.</p>
<pre><code class="language-text">Database Transaction
├── Business Data
└── Outbox Event
</code></pre>
<p>Message publishing happens afterward.</p>
<p>The goal is not to make the database and Message Broker participate in the same transaction, but to make the database operation itself reliable and then handle message delivery separately.</p>
<h2>Outbox vs. Direct Event Publishing</h2>
<p>The simple approach is:</p>
<pre><code class="language-text">UPDATE DB
   ↓
PUBLISH EVENT
</code></pre>
<h3>Advantages</h3>
<ul>
<li><p>Simple</p>
</li>
<li><p>Low cost</p>
</li>
<li><p>Low latency</p>
</li>
</ul>
<h3>Disadvantages</h3>
<ul>
<li><p>Dual Write</p>
</li>
<li><p>Potential Lost Event</p>
</li>
<li><p>Failure Window</p>
</li>
</ul>
<p>The Outbox approach is:</p>
<pre><code class="language-text">UPDATE DB
   +
INSERT OUTBOX
   ↓
COMMIT
   ↓
PUBLISH ASYNC
</code></pre>
<h3>Advantages</h3>
<ul>
<li><p>Higher reliability</p>
</li>
<li><p>Easier retries</p>
</li>
<li><p>No Lost Event in common failures between Commit and Publish</p>
</li>
</ul>
<h3>Costs</h3>
<ul>
<li><p>Increased complexity</p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Eventual_consistency">Eventual Consistency</a></p>
</li>
<li><p>Duplicate handling</p>
</li>
<li><p>Operational overhead</p>
</li>
</ul>
<p>Therefore, if an Event is not actually critical, the Outbox Pattern is not necessarily required.</p>
<p>For example, if Events are used only for non-critical analytics and losing a small percentage of them is acceptable, keeping the architecture simple may be more valuable.</p>
<h2>Is the Outbox Pattern Only for Microservices ?</h2>
<p>No.</p>
<p>A common misconception is that the Outbox Pattern is exclusively a Microservices pattern.</p>
<p>Even in a <a href="https://www.geeksforgeeks.org/system-design/monolithic-architecture-system-design/">Monolithic Architecture</a>, you may need to perform an external operation after changing the database.</p>
<p>For example:</p>
<pre><code class="language-text">Order Created
     ↓
Call a Webhook
</code></pre>
<p>Or:</p>
<pre><code class="language-text">User Registered
     ↓
Send Email
</code></pre>
<p>If the Email Provider is temporarily unavailable, you may not want to fail the user's registration.</p>
<p>Instead, you can store:</p>
<pre><code class="language-text">User
+
Email Event
</code></pre>
<p>inside the same transaction and send the email asynchronously afterward.</p>
<p>So the Outbox Pattern is better understood as a reliable solution for <a href="https://aws.amazon.com/blogs/compute/understanding-asynchronous-messaging-for-microservices/">Asynchronous Messaging</a> rather than something limited to Microservices.</p>
<h2>When Is the Outbox Pattern Worth Using ?</h2>
<p>The Outbox Pattern is particularly valuable when:</p>
<ul>
<li><p>The Event is important to the business.</p>
</li>
<li><p>Losing an Event is unacceptable.</p>
</li>
<li><p>The database and Broker are independent systems.</p>
</li>
<li><p>The architecture is Event-Driven.</p>
</li>
<li><p>Services communicate asynchronously.</p>
</li>
<li><p>Retry and reliability are important.</p>
</li>
<li><p>Immediate consistency between services is not required.</p>
</li>
</ul>
<h2>When Isn't the Outbox Pattern Necessary ?</h2>
<p>If an Event is completely non-critical, adding an Outbox may introduce unnecessary complexity.</p>
<p>For example:</p>
<pre><code class="language-text">User clicked button
</code></pre>
<p>Suppose this Event is used only for approximate analytics.</p>
<p>If losing some Events is acceptable, direct publishing—or even an independent analytics system—may be a simpler solution.</p>
<p>The right architecture depends on the business consequences of failure.</p>
<h2>Technical Considerations When Implementing Outbox</h2>
<h3>1. Use a Unique Event ID</h3>
<p>Every Event should have a stable identifier:</p>
<pre><code class="language-text">event_id = UUID
</code></pre>
<p>This ID is essential for Deduplication.</p>
<h3>2. Design Consumers to Be Idempotent</h3>
<p>Suppose:</p>
<pre><code class="language-text">OrderPaid
</code></pre>
<p>is delivered twice.</p>
<p>The result should not be:</p>
<pre><code class="language-text">Payment = Payment × 2
</code></pre>
<p>The Consumer needs to recognize that the Event has already been processed.</p>
<h3>3. Index the Outbox</h3>
<p>If you use polling, queries such as:</p>
<pre><code class="language-sql">WHERE processed_at IS NULL ORDER BY created_at
</code></pre>
<p>should be supported by appropriate indexes based on your workload and access patterns.</p>
<h3>4. Consider Batch Processing</h3>
<p>Instead of:</p>
<pre><code class="language-text">1 Event → 1 Query
</code></pre>
<p>you may be better off processing:</p>
<pre><code class="language-text">100 Events → 1 Batch
</code></pre>
<p>The <a href="https://aws.amazon.com/what-is/batch-processing/">AWS Batch Processing guidance</a> provides useful background on batch processing.</p>
<p>However, very large batches can also increase:</p>
<ul>
<li><p>Lock duration</p>
</li>
<li><p>Memory usage</p>
</li>
<li><p>Retry costs</p>
</li>
</ul>
<p>So the batch size should be chosen carefully.</p>
<h3>5. Use Retry with Backoff</h3>
<p>If the Broker is unavailable, you should not continuously retry every few milliseconds.</p>
<p>For example:</p>
<pre><code class="language-text">1s
2s
4s
8s
16s
...
</code></pre>
<p>Use exponential backoff together with appropriate retry limits and policies.</p>
<h3>6. Don't Forget <a href="https://www.linkedin.com/posts/mehradsadeghi_softwarearchitecture-distributedsystems-microservices-share-7499746869149884416-5ha3/">Dead-Letter</a> Handling</h3>
<p>Sometimes an Event will consistently fail.</p>
<p>If you retry indefinitely:</p>
<pre><code class="language-text">Event
 ↓
Fail
 ↓
Retry
 ↓
Fail
 ↓
Retry
 ↓
...
</code></pre>
<p>you can create a <a href="https://learn.microsoft.com/en-us/azure/architecture/antipatterns/retry-storm/"><strong>Retry Storm</strong></a>.</p>
<p>A problematic Event therefore needs an explicit failure-handling strategy, such as a Dead Letter mechanism.</p>
<h2>An Important Note About Monitoring</h2>
<p>One of the most useful Outbox metrics is the number of Events waiting to be published.</p>
<p>For example:</p>
<pre><code class="language-text">Outbox Pending Events = 12
</code></pre>
<p>might be completely normal.</p>
<p>But:</p>
<pre><code class="language-text">Outbox Pending Events = 2,000,000
</code></pre>
<p>could indicate that the Publisher has fallen significantly behind.</p>
<p>Useful metrics can include:</p>
<pre><code class="language-text">Outbox Backlog
Publish Latency
Publish Failure Rate
Retry Count
Oldest Unprocessed Event Age
Consumer Lag
Dead Letter Count
</code></pre>
<p>One particularly valuable metric is:</p>
<pre><code class="language-text">Oldest Unprocessed Event Age
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Age = 45 minutes
</code></pre>
<p>Even if the total number of pending Events is relatively small, an old unprocessed Event could indicate an operational problem.</p>
<h2>The Outbox Pattern and Eventual Consistency</h2>
<p>The Outbox Pattern forces us to accept an important reality of distributed systems:</p>
<blockquote>
<p>Not everything needs to be immediately consistent.</p>
</blockquote>
<p>For example:</p>
<pre><code class="language-text">T0:
Order Created

T0 + 20ms:
Outbox Published

T0 + 50ms:
Inventory Updated

T0 + 100ms:
Notification Sent
</code></pre>
<p>For a short period, the system might look like this:</p>
<pre><code class="language-text">Order Service     → CREATED
Inventory Service → Previous State
</code></pre>
<p>This is not necessarily a bug.</p>
<p>If your business requirements allow this delay, Eventual Consistency can be a perfectly reasonable trade-off.</p>
<p>In Event-Driven Architectures, this is one of the fundamental design considerations.</p>
<p>Duplication, ordering, and idempotency all need to be deliberately designed.</p>
<h2>Final Thoughts</h2>
<p>The <strong>Outbox Pattern</strong> is a solution to one of the classic problems in distributed systems: the <strong>Dual Write Problem</strong>.</p>
<p>Instead of having an application simultaneously modify:</p>
<pre><code class="language-text">Database
+
Message Broker
</code></pre>
<p>we store the business state change and its corresponding Event in a single local database transaction:</p>
<pre><code class="language-text">Database
├── Business Data
└── Outbox Event
</code></pre>
<p>A separate process then transfers the Event to the Message Broker.</p>
<p>The main benefit is straightforward:</p>
<blockquote>
<p>If the original transaction commits, the corresponding Event is also persisted and can be retried later.</p>
</blockquote>
<p>But the Outbox Pattern is not the end of the story.</p>
<p>You still need to deal with:</p>
<ul>
<li><p>Duplicate messages</p>
</li>
<li><p>Idempotency</p>
</li>
<li><p>Ordering</p>
</li>
<li><p>Eventual Consistency</p>
</li>
<li><p>Retries</p>
</li>
<li><p>Outbox Growth</p>
</li>
<li><p>Monitoring</p>
</li>
<li><p>Cleanup</p>
</li>
</ul>
<p>In other words, the Outbox Pattern does not eliminate distributed-system complexity.</p>
<p>It moves that complexity into areas where it can be managed more explicitly.</p>
<h2>A Question for Discussion</h2>
<p>If losing an Event is unacceptable for your system, but a few seconds of Eventual Consistency is tolerable, would you introduce the additional complexity of the Outbox Pattern, retries, and idempotency ?</p>
<p>And at what point would you decide that architectural simplicity is more valuable than higher messaging reliability ?</p>
<h2>Further Reading</h2>
<ul>
<li><p><a href="https://microservices.io/patterns/data/transactional-outbox.html">Transactional Outbox — Microservices.io</a></p>
</li>
<li><p><a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html">AWS Prescriptive Guidance — Transactional Outbox</a></p>
</li>
<li><p><a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html">Debezium — Outbox Event Router</a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/architecture/databases/guide/transactional-out-box-cosmos">Microsoft Azure Architecture Center — Transactional Outbox</a></p>
</li>
<li><p><a href="https://dev.to/igornosatov_15/the-outbox-pattern-a-love-letter-to-eventual-consistency-3ch3">The Outbox Pattern: A Love Letter to Eventual Consistency</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Circuit Breaker Pattern Explained: How to Prevent Cascading Failures in Distributed Systems]]></title><description><![CDATA[What happens when one service fails, but your entire system keeps calling it anyway ?
Imagine your payment service is down. Normally, every purchase request follows a path like this:
User
 ↓
Order Ser]]></description><link>https://mehradsadeghi.hashnode.dev/circuit-breaker-pattern-prevent-cascading-failures-in-distributed-systems</link><guid isPermaLink="true">https://mehradsadeghi.hashnode.dev/circuit-breaker-pattern-prevent-cascading-failures-in-distributed-systems</guid><category><![CDATA[Microservices]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Circuit breaker pattern]]></category><dc:creator><![CDATA[Mehrad]]></dc:creator><pubDate>Wed, 16 Sep 2026 13:48:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/b7d6015d-123d-4239-906d-ca8121b2b37e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>What happens when one service fails, but your entire system keeps calling it anyway ?</strong></p>
<p>Imagine your payment service is down. Normally, every purchase request follows a path like this:</p>
<pre><code class="language-plaintext">User
 ↓
Order Service
 ↓
Payment Service
 ↓
Bank
</code></pre>
<p>Now the Payment Service is unavailable — or perhaps it’s simply extremely slow. A request that normally takes <strong>200 ms</strong> now takes <strong>10 seconds</strong>. If your system continues sending requests to it, what happens ?</p>
<ul>
<li><p>Request queues grow.</p>
</li>
<li><p>Threads and connections remain occupied.</p>
</li>
<li><p>Timeouts increase.</p>
</li>
<li><p>Latency goes up.</p>
</li>
</ul>
<p>And eventually, services that were perfectly healthy can start failing too.</p>
<p>In other words, <strong>one failing dependency can potentially bring down an entire system.</strong></p>
<p>This is where the <a href="https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern"><strong>Circuit Breaker pattern</strong></a> comes in.</p>
<blockquote>
<p>Instead of continuously calling a dependency that is already failing, the system temporarily stops sending requests to it and <strong>fails fast</strong>.</p>
</blockquote>
<p>The goal isn’t to fix the broken service. The goal is to <strong>prevent its failure from spreading</strong>.</p>
<h2><strong>What Is the Circuit Breaker Pattern ?</strong></h2>
<p>The Circuit Breaker is a <a href="https://www.geeksforgeeks.org/system-design/resilient-distributed-systems/">resilience</a> pattern commonly used in distributed systems to protect services from repeatedly calling an unhealthy dependency.</p>
<p>The idea comes from electrical circuit breakers. When electrical current becomes dangerously high, a circuit breaker interrupts the circuit to prevent further damage.</p>
<p>Software can apply a similar idea:</p>
<pre><code class="language-plaintext">Healthy dependency
↓
Requests flow
↓
Dependency starts failing
↓
Failure threshold reached
↓
Circuit opens
↓
Requests fail fast
</code></pre>
<p>A Circuit Breaker doesn’t repair the dependency.</p>
<p>Instead, it can:</p>
<ul>
<li><p>Stop unnecessary requests to a failing service</p>
</li>
<li><p>Protect your own application’s resources</p>
</li>
<li><p>Reduce additional load on the unhealthy dependency</p>
</li>
<li><p>Help prevent <a href="https://medium.com/@ninadwalanj/how-failure-cascades-in-distributed-systems-eccc48c9851a">cascading failures</a></p>
</li>
<li><p>Give the dependency time to <a href="https://www.geeksforgeeks.org/operating-systems/recovery-in-distributed-systems/">recover</a></p>
</li>
<li><p>Allow the system to <a href="https://www.geeksforgeeks.org/system-design/graceful-degradation-in-distributed-systems/">degrade gracefully</a> when possible</p>
</li>
</ul>
<p>A useful way to think about it is:</p>
<blockquote>
<p>Circuit Breaker doesn’t eliminate failure. It prevents failure from becoming a larger system-wide failure.</p>
</blockquote>
<h2><strong>Why Do Distributed Systems Need Circuit Breakers ?</strong></h2>
<p>In a simple <a href="https://www.geeksforgeeks.org/system-design/monolithic-architecture-system-design/">monolithic application</a>, you might call a function like:</p>
<pre><code class="language-plaintext">calculatePrice()
</code></pre>
<p>If the function fails, handling the failure is usually relatively straightforward. But <a href="https://en.wikipedia.org/wiki/Distributed_computing">distributed systems</a> are different.</p>
<p>A call to another service crosses a network boundary:</p>
<pre><code class="language-plaintext">Service A
↓
Service B
↓
Service C
↓
External APIService A
↓
Service B
↓
Service C
↓
External API
</code></pre>
<p>Unlike a local function call, a remote call can:</p>
<ul>
<li><p>Time out</p>
</li>
<li><p>Lose its connection</p>
</li>
<li><p>Become extremely slow</p>
</li>
<li><p>Return a 5xx error</p>
</li>
<li><p>Be rate-limited</p>
</li>
<li><p>Become completely unavailable</p>
</li>
</ul>
<p>And the more important problem is that <strong>failure can propagate between services</strong>.</p>
<p>For example:</p>
<pre><code class="language-plaintext">Payment Service
↓
starts failing
↓
Order Service waits
↓
Threads become occupied
↓
Requests start queuing
↓
Latency increases
↓
Error rate increases
↓
The entire service comes under pressure
</code></pre>
<p>This is a <strong>cascading failure</strong>.</p>
<p>The original problem may have started in one service, but the resulting resource exhaustion can spread to otherwise healthy parts of the system.</p>
<p>Circuit Breaker is one of the patterns that can help interrupt this chain.</p>
<h3><strong>Reliability, High Availability, and Circuit Breakers</strong></h3>
<p>Two concepts often appear in discussions about distributed systems:</p>
<p><strong>Reliability:</strong> is about a system behaving dependably in the presence of failures and recovering appropriately when failures occur.</p>
<p><strong>High Availability:</strong> is about keeping a system accessible to users even when parts of its infrastructure or dependencies experience problems.</p>
<p>A Circuit Breaker does <strong>not</strong> make an external service permanently available.</p>
<p>Instead, it can help isolate its failure:</p>
<pre><code class="language-plaintext">Dependency Failure
↓
Circuit Breaker
↓
Failure Isolation
↓
Graceful Degradation
↓
System remains responsive
</code></pre>
<p>This is where the idea of <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/resilience-analysis-framework/overview.html"><strong>failure isolation</strong></a> becomes important.</p>
<p>Instead of allowing one dependency’s failure to spread across the entire system, we try to limit its <strong>blast radius</strong>.</p>
<h2><strong>How Does a Circuit Breaker Work ?</strong></h2>
<p>A Circuit Breaker is commonly modeled as a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">state machine</a> with three primary states:</p>
<ul>
<li><p>Closed</p>
</li>
<li><p>Open</p>
</li>
<li><p>Half-Open</p>
</li>
</ul>
<h3><strong>CLOSED - Everything Is Normal</strong></h3>
<p>When the Circuit Breaker is <strong>CLOSED</strong>, requests are allowed to reach the dependency.</p>
<pre><code class="language-plaintext">Client
↓
Circuit Breaker
↓
Payment Service
</code></pre>
<p>The Circuit Breaker monitors the results of those requests. For example:</p>
<pre><code class="language-plaintext">100 requests
95 successes
5 failures
</code></pre>
<p>If the failure rate exceeds the configured threshold, the circuit transitions to OPEN.</p>
<p>But an important detail is that a failure threshold doesn’t necessarily mean:</p>
<blockquote>
<p>Five requests failed consecutively.</p>
</blockquote>
<p>The threshold can be based on different measurements, such as:</p>
<ul>
<li><p>Number of failures</p>
</li>
<li><p>Failure percentage</p>
</li>
<li><p>Number of timeouts</p>
</li>
<li><p>Failures within a time window</p>
</li>
<li><p>A combination of multiple metrics</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-plaintext">50 failures in the last 100 requests
</code></pre>
<p>or:</p>
<pre><code class="language-plaintext">Failure Rate &gt; 30% during the last 30 seconds
</code></pre>
<p>The right threshold depends on the actual behavior of the dependency and the reliability <a href="https://en.wikipedia.org/wiki/Service-level_objective">objectives</a> of your system. There is no universal magic number.</p>
<h3><strong>OPEN - Stop Calling the Failing Service</strong></h3>
<p>When the Circuit Breaker transitions to <strong>OPEN</strong>, its behavior changes completely.</p>
<p>Before:</p>
<pre><code class="language-plaintext">Request
↓
Circuit Breaker
↓
Payment Service
</code></pre>
<p>After:</p>
<pre><code class="language-plaintext">Request
↓
Circuit Breaker
↓
FAIL FAST
</code></pre>
<p>The request never reaches the Payment Service. This is the central idea behind the Circuit Breaker pattern. If we already have strong evidence that a dependency is unhealthy, why should every new request wait for another network timeout ?</p>
<p>Suppose the Payment Service has a <strong>10-second timeout</strong>. Now imagine 1,000 concurrent requests all waiting for that timeout:</p>
<pre><code class="language-plaintext">1,000 requests × 10 seconds
</code></pre>
<p>A significant amount of your application’s resources can remain occupied simply waiting for a dependency that is already failing.</p>
<p>With an open circuit:</p>
<pre><code class="language-plaintext">Request
↓
Circuit = OPEN
↓
Immediate failure
</code></pre>
<p>The system can reject the request much earlier.</p>
<p>That can help:</p>
<ul>
<li><p>Free threads</p>
</li>
<li><p>Preserve connections</p>
</li>
<li><p>Reduce resource consumption</p>
</li>
<li><p>Keep latency more predictable</p>
</li>
<li><p>Prevent additional traffic from reaching the unhealthy dependency</p>
</li>
</ul>
<p>Sometimes <strong>failing quickly is better than failing slowly</strong>.</p>
<p>But the circuit should not remain open forever. If it did, the system would never discover that the dependency has recovered.</p>
<p>After a configured period, the Circuit Breaker can transition to HALF-OPEN.</p>
<h3><strong>HALF-OPEN - Has the Service Recovered ?</strong></h3>
<p>This is one of the most subtle parts of the pattern.</p>
<p>Suppose the Payment Service was unavailable, but now appears to be healthy again. Should we immediately send all traffic back to it ?</p>
<p><strong>Not necessarily</strong>. The service may have recovered only partially. For example, it might have just restarted and currently be capable of handling only a small amount of traffic.</p>
<p>Instead of immediately allowing thousands of requests through, the Circuit Breaker can allow a limited number of <strong>test requests</strong>. For example:</p>
<pre><code class="language-plaintext">HALF-OPEN
↓
5 test requests
↓
Are they successful ?
</code></pre>
<p>If the requests succeed:</p>
<pre><code class="language-plaintext">HALF-OPEN
↓
Successful requests
↓
CLOSED
</code></pre>
<p>The circuit returns to normal operation.</p>
<p>If the test requests fail:</p>
<pre><code class="language-plaintext">HALF-OPEN
↓
Failure
↓
OPEN
</code></pre>
<p>The circuit opens again.</p>
<h2><strong>Why Does HALF-OPEN Allow Only a Few Requests ?</strong></h2>
<p>Because <strong>service recovery doesn’t necessarily mean full recovery</strong>. Imagine a Payment Service that was down and has just come back online. It might currently be capable of processing only:</p>
<pre><code class="language-plaintext">20 requests / second
</code></pre>
<p>If your system suddenly sends:</p>
<pre><code class="language-plaintext">10,000 requests
</code></pre>
<p>you could create another overload:</p>
<pre><code class="language-plaintext">Recovery
↓
Traffic spike
↓
Overload
↓
Failure again
</code></pre>
<p>In other words, your system can accidentally overwhelm the dependency immediately after it recovers.</p>
<p>Half-Open provides a controlled way to test recovery:</p>
<p>Instead of:</p>
<pre><code class="language-plaintext">0 requests
↓
10,000 requests
</code></pre>
<p>we can do:</p>
<pre><code class="language-plaintext">0 requests
↓
A few test requests
↓
Evaluate results
↓
Gradually increase traffic
</code></pre>
<p>The exact number of test requests is implementation- and system-dependent. There is no universal value that works for every architecture.</p>
<h2><strong>A Real-World Scenario: Payment Service Failure</strong></h2>
<p>Let’s put everything together. Imagine an online store:</p>
<pre><code class="language-plaintext">User
↓
Order Service
↓
Payment Service
↓
Bank Gateway
</code></pre>
<p>Under normal conditions:</p>
<pre><code class="language-plaintext">Order Service
↓
Circuit Breaker
↓
Payment Service
↓
Bank
</code></pre>
<p>Everything is working normally.</p>
<p><strong>Step 1: The Failure Starts</strong></p>
<p>The Bank Gateway begins experiencing problems. The Payment Service starts timing out:</p>
<pre><code class="language-plaintext">Payment Request
↓
Timeout
</code></pre>
<p>The Circuit Breaker records these failures.</p>
<p><strong>Step 2: The Failure Threshold Is Reached</strong></p>
<p>Suppose our policy is:</p>
<pre><code class="language-plaintext">100 requests
40 failures
</code></pre>
<p>and our configured rule is:</p>
<pre><code class="language-plaintext">Failure Rate &gt; 30% → OPEN
</code></pre>
<p>The threshold has been exceeded. The Circuit Breaker opens.</p>
<p><strong>Step 3: New Requests Fail Fast</strong></p>
<p>Now a new request arrives:</p>
<pre><code class="language-plaintext">User
↓
Order Service
↓
Circuit = OPEN
↓
Immediate Failure
</code></pre>
<p>The Order Service no longer calls the Payment Service.</p>
<p>This prevents the application from repeatedly waiting for a dependency that is already known to be unhealthy.</p>
<p><strong>Step 4: The System</strong> <a href="https://www.geeksforgeeks.org/system-design/graceful-degradation-in-distributed-systems/"><strong>Degrades Gracefully</strong></a></p>
<p>Once the circuit is open, the system still has to decide what the user should experience. For example:</p>
<blockquote>
<p>Payment is temporarily unavailable. Please try again in a few minutes.</p>
</blockquote>
<p>Or the application might have another valid strategy:</p>
<ul>
<li><p>Keep the order in a PENDING state</p>
</li>
<li><p>Put the operation into a queue</p>
</li>
<li><p>Switch to another payment provider</p>
</li>
<li><p>Return non-critical information from a cache</p>
</li>
</ul>
<p>This is where <strong>fallback</strong> becomes important.</p>
<h2><strong>What Is a Fallback?</strong></h2>
<p>A fallback answers a simple question:</p>
<blockquote>
<p><strong>If the primary path fails, do we have a meaningful alternative ?</strong></p>
</blockquote>
<p>For example:</p>
<pre><code class="language-plaintext">Primary Payment Provider
↓
FAILED
↓
Secondary Payment Provider
</code></pre>
<p>But a fallback is not always possible. For a payment operation, you cannot simply return <em>Payment successful,</em> when the payment was never actually processed.</p>
<p>The fallback must therefore be <strong>consistent with the business logic</strong>. A fallback is not a fake success. It is an alternative behavior that is safe and meaningful for that particular operation.</p>
<h2><strong>Circuit Breaker ≠ Retry</strong></h2>
<p>These two concepts are often confused. They solve different problems.</p>
<p><strong>Retry says:</strong></p>
<blockquote>
<p>The request failed. Maybe trying again will succeed.</p>
</blockquote>
<p><strong>Circuit Breaker says:</strong></p>
<blockquote>
<p>This dependency has been failing repeatedly. Stop sending requests to it for now.</p>
</blockquote>
<p>Retry can be useful for <a href="https://learn.microsoft.com/en-us/azure/architecture/best-practices/transient-faults"><strong>transient failures</strong></a>, such as:</p>
<ul>
<li><p>Temporary network glitch</p>
</li>
<li><p>Temporary timeout</p>
</li>
<li><p>Temporary throttling</p>
</li>
</ul>
<p>But if the dependency is genuinely down, excessive retries can make the situation worse.</p>
<h2><strong>How Retries Can Cause a Cascading Failure</strong></h2>
<p>Suppose you have 1,000 requests and every failed request is retried three times. You might end up with:</p>
<pre><code class="language-plaintext">1,000 original requests + 3,000 retries = 4,000 requests
</code></pre>
<p>That’s exactly what you don’t want when the dependency is already overloaded.</p>
<p>Now imagine:</p>
<pre><code class="language-plaintext">A → B → C
</code></pre>
<p>Service C fails. Service B retries its requests to C. Service A retries its requests to B.</p>
<p>The result can look like this:</p>
<pre><code class="language-plaintext">C fails
↓
B retries
↓
A retries
↓
More traffic
↓
C becomes even more overloaded
↓
More failures
↓
Cascading failure
</code></pre>
<p>This can lead to a <a href="https://learn.microsoft.com/en-us/azure/architecture/antipatterns/retry-storm/"><strong>retry storm</strong></a>.</p>
<p><strong>So should we eliminate retries ?</strong></p>
<p><strong>No</strong>. The problem isn’t retrying itself. The problem is <strong>uncontrolled retries</strong>. A more resilient design might combine:</p>
<pre><code class="language-plaintext">Request
↓
Timeout
↓
Retry
↓
Exponential Backoff + Jitter
↓
Circuit Breaker
</code></pre>
<p>The important point is that retries should generally be limited to failures where another attempt has a reasonable chance of succeeding.</p>
<p>For example, retrying a transient 503 Service Unavailable may make sense in some systems.</p>
<p>Retrying a 401 Unauthorized generally does not solve the underlying problem.</p>
<h3><strong>Exponential Backoff and Jitter</strong></h3>
<p>Imagine 10,000 requests fail at approximately the same time. If every client retries exactly one second later:</p>
<pre><code class="language-plaintext">1 second
↓
10,000 requests
</code></pre>
<p>You’ve created another traffic spike.</p>
<p>With <a href="https://dilankam.medium.com/understanding-retries-exponential-backoffs-and-circuit-breakers-in-distributed-systems-4355db103505"><strong>exponential backoff</strong></a>, the delay between retries increases:</p>
<pre><code class="language-plaintext">1s
2s
4s
8s
…
</code></pre>
<p>This helps spread retries over a longer period.</p>
<p>But there’s still a problem. If every client follows exactly the same schedule, they can still retry at roughly the same moments. That’s where <a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/"><strong>jitter</strong></a> helps. Instead of:</p>
<pre><code class="language-plaintext">1s
2s
4s
8s
</code></pre>
<p>different clients might retry at slightly different times:</p>
<pre><code class="language-plaintext">1.2s
1.8s
2.4s
3.1s
…
</code></pre>
<p>The goal is to spread retry traffic instead of allowing thousands of clients to synchronize their retries.</p>
<h2><strong>Idempotency: The Problem You Must Consider When Retrying</strong></h2>
<p>Retries introduce another important problem: <strong>duplicate side effects</strong>. Imagine:</p>
<pre><code class="language-http">POST /payments
</code></pre>
<p>The request reaches the Payment Service. The payment is successfully processed. But the response is lost because of a network failure. What does the client see?</p>
<pre><code class="language-plaintext">Timeout
</code></pre>
<p>From the client’s perspective, it doesn’t know whether the payment succeeded. So it retries:</p>
<pre><code class="language-http">POST /payments
</code></pre>
<p>Now you potentially have:</p>
<pre><code class="language-plaintext">Request #1 → Payment successful
↓
Response lost
↓
Retry
↓
Request #2 → Payment successful again
</code></pre>
<p>You may have charged the customer twice. This is where <strong>idempotency</strong> becomes critical. For example, the client could send an idempotency key:</p>
<pre><code class="language-plaintext">Idempotency-Key: ABC123
</code></pre>
<p>The Payment Service can use that key to recognize that the same logical operation has already been processed.</p>
<p>Conceptually:</p>
<pre><code class="language-plaintext">Retry
↓
Same Idempotency Key
↓
Same Logical Operation
↓
No Duplicate Side Effect
</code></pre>
<p>Therefore, whenever you introduce retries, ask:</p>
<blockquote>
<p>Is this operation actually safe to retry ?</p>
</blockquote>
<h2><strong>Important Circuit Breaker Configuration Parameters</strong></h2>
<p>Implementing a Circuit Breaker isn’t simply a matter of adding an if statement. Several parameters need to be defined.</p>
<p><strong>1. Failure Threshold</strong></p>
<p>How many failures should cause the circuit to open ? For example:</p>
<pre><code class="language-plaintext">Failure Rate &gt; 50%
</code></pre>
<p>A threshold that is too low may cause the circuit to open because of temporary failures.</p>
<p>A threshold that is too high may allow the dependency to cause significant damage before the circuit reacts.</p>
<p><strong>2. Recovery Timeout</strong></p>
<p>Once the circuit is open, how long should we wait before testing the dependency again ?</p>
<p>For example:</p>
<pre><code class="language-plaintext">30 seconds
</code></pre>
<p>A timeout that is too short can produce this cycle:</p>
<pre><code class="language-plaintext">OPEN
↓
HALF-OPEN
↓
Failure
↓
OPEN
↓
HALF-OPEN
↓
…
</code></pre>
<p>On the other hand, a timeout that is too long may prevent requests from reaching a dependency even after it has recovered.</p>
<p><strong>3. Number of HALF-OPEN Requests</strong></p>
<p>When the circuit enters HALF-OPEN, how many requests should be allowed through ?</p>
<p>For example:</p>
<pre><code class="language-plaintext">1 request
</code></pre>
<p>or:</p>
<pre><code class="language-plaintext">5 requests
</code></pre>
<p>or a limited percentage of traffic.</p>
<p>Allowing more requests can provide more information about recovery, but it also creates more load on the recovering dependency.</p>
<p><strong>4. Which Failures Should Open the Circuit ?</strong></p>
<p>Not every error should necessarily be treated as evidence that a dependency is unhealthy.</p>
<p>For example:</p>
<pre><code class="language-plaintext">Timeout → likely relevant
503 → likely relevant
Connection error → likely relevant
401 → probably not
400 → probably not
</code></pre>
<p>If every 4xx error contributes to the Circuit Breaker threshold, you could incorrectly conclude that the entire dependency is unavailable.</p>
<h2><strong>Circuit Breaker Has a Cost Too</strong></h2>
<p>No resilience pattern is free. Circuit Breaker provides important benefits, but it also introduces complexity.</p>
<p><strong>Potential benefits</strong></p>
<ul>
<li><p>Helps prevent cascading failures</p>
</li>
<li><p>Reduces load on an unhealthy dependency</p>
</li>
<li><p>Enables fail-fast behavior</p>
</li>
<li><p>Protects application resources</p>
</li>
<li><p>Supports <a href="https://www.geeksforgeeks.org/system-design/graceful-degradation-in-distributed-systems/">graceful degradation</a></p>
</li>
<li><p>Improves system resilience</p>
</li>
<li><p>Enables controlled recovery</p>
</li>
</ul>
<p><strong>Potential costs</strong></p>
<ul>
<li><p>More application complexity</p>
</li>
<li><p>Additional monitoring requirements</p>
</li>
<li><p>Threshold tuning</p>
</li>
<li><p>State management</p>
</li>
<li><p>Fallback design</p>
</li>
<li><p>More difficult debugging</p>
</li>
<li><p><a href="https://www.testdevlab.com/blog/false-positives-and-negatives-in-software-testing">False positives</a></p>
</li>
<li><p><a href="https://www.testdevlab.com/blog/false-positives-and-negatives-in-software-testing">False negatives</a></p>
</li>
</ul>
<p>For example, if your threshold is too sensitive:</p>
<pre><code class="language-plaintext">Temporary failure
↓
Circuit opens
↓
Requests rejected
</code></pre>
<p>even though the dependency wasn’t actually down. If the threshold is too permissive:</p>
<pre><code class="language-plaintext">Dependency is failing
↓
Circuit remains CLOSED
↓
More failures
↓
More resource consumption
</code></pre>
<p>So configuring a Circuit Breaker is a <strong>trade-off</strong>, not a search for one perfect number.</p>
<h2><strong>Where Should the Circuit Breaker Live ?</strong></h2>
<p>A common placement is close to the remote call:</p>
<pre><code class="language-plaintext">Order Service
↓
Circuit Breaker
↓
Payment Service
</code></pre>
<p>This keeps the decision close to the dependency being protected.</p>
<p>However, Circuit Breaking can also be implemented at other layers, depending on the architecture:</p>
<ul>
<li><p>Application layer</p>
</li>
<li><p>API Gateway</p>
</li>
<li><p><a href="https://aws.amazon.com/what-is/service-mesh/">Service Mesh</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/sidecar-design-pattern-for-microservices/">Sidecar</a></p>
</li>
</ul>
<p>A Service Mesh for example, can provide resilience features without requiring every application to implement the same logic. But that introduces another trade-off. Moving more resilience behavior into infrastructure can simplify application code, while potentially making debugging and understanding system behavior more complicated.</p>
<h2><strong>Do You Always Need a Circuit Breaker ?</strong></h2>
<p><strong>No</strong>.</p>
<p>Circuit Breaker is not automatically appropriate for every operation.</p>
<p>For example:</p>
<pre><code class="language-javascript">function calculateTax()
</code></pre>
<p>If this is a local, fast operation with no meaningful remote dependency, adding a Circuit Breaker may simply introduce unnecessary complexity.</p>
<p>Circuit Breakers become more interesting when dealing with things such as:</p>
<ul>
<li><p>Remote APIs</p>
</li>
<li><p>External providers</p>
</li>
<li><p>Database dependencies</p>
</li>
<li><p><a href="https://medium.com/@AI-Simplified/distributed-systems-and-microservices-guide-to-streamlined-software-architectures-2782673e5808">Microservices</a></p>
</li>
<li><p>Third-party services</p>
</li>
</ul>
<p>The key question isn’t:</p>
<blockquote>
<p>Do we use microservices ?</p>
</blockquote>
<p>The better question is:</p>
<blockquote>
<p><strong>What happens to our system when this dependency becomes slow, unavailable, or unreliable ?</strong></p>
</blockquote>
<h2><strong>Circuit Breaker Alone Is Not Resilience</strong></h2>
<p>One common misconception is Circuit Breaker = Resilience. It isn’t.</p>
<p>Circuit Breaker is only <strong>one tool</strong> in a broader resilience strategy.</p>
<p>A more complete design might look like:</p>
<pre><code class="language-plaintext">     ┌─────────────┐
     │   Timeout   │
     └──────┬──────┘
            ↓
     ┌─────────────┐
     │    Retry    │
     └──────┬──────┘
            ↓
┌───────────────────────┐
│ Exponential Backoff   │
│       + Jitter        │
└───────────┬───────────┘
            ↓
    ┌─────────────┐
    │   Circuit   │
    │   Breaker   │
    └──────┬──────┘
           ↓
    ┌─────────────┐
    │  Fallback   │
    └─────────────┘
</code></pre>
<p>Depending on the system, other patterns and mechanisms may also be relevant:</p>
<ul>
<li><p><a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/bulkhead">Bulkheads</a></p>
</li>
<li><p><a href="https://dev.to/smiah/rate-limiting-in-distributed-system-3h59">Rate limiting</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/distributed-task-queue-distributed-systems/">Queues</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Distributed_cache">Caching</a></p>
</li>
<li><p><a href="https://copyconstruct.medium.com/health-checks-in-distributed-systems-aa8a0e8c1672">Health checks</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/observability-in-distributed-systems/">Observability</a></p>
</li>
<li><p><a href="https://dzone.com/articles/importance-of-idempotency-in-distributed-systems">Idempotency</a></p>
</li>
</ul>
<p>Resilience usually comes from <strong>combining the right mechanisms</strong>, not from adding one pattern everywhere.</p>
<h2><strong>How Do You Know Your Circuit Breaker Is Working ?</strong></h2>
<p>A Circuit Breaker without observability is difficult to operate. At a minimum, you should be able to monitor metrics such as:</p>
<ul>
<li><p>Circuit state</p>
</li>
<li><p>Failure rate</p>
</li>
<li><p>Success rate</p>
</li>
<li><p>Timeout rate</p>
</li>
<li><p>Number of circuit openings</p>
</li>
<li><p>Half-Open attempts</p>
</li>
<li><p>Recovery time</p>
</li>
<li><p>Fallback rate</p>
</li>
<li><p>Request latency</p>
</li>
</ul>
<p>State transitions can also be important monitoring events. For example:</p>
<pre><code class="language-plaintext">Circuit: CLOSED → OPEN
</code></pre>
<p>This may indicate a significant problem with an external dependency.</p>
<p>Without monitoring, you may know that requests are failing — but not know <strong>why the Circuit Breaker opened, how often it opens, or whether it is actually helping.</strong></p>
<h3><strong>The Goal Isn’t to Eliminate Errors</strong></h3>
<p>This is one of the most important ideas behind the pattern. A Circuit Breaker does <strong>not necessarily reduce the number of errors users see</strong>. In fact, after a circuit opens, you may see more immediate failures. But those failures can be much more controlled.</p>
<p>Without a Circuit Breaker:</p>
<pre><code class="language-plaintext">1,000 requests
↓
1,000 timeouts
↓
10 seconds each
↓
Thread exhaustion
↓
Entire service becomes unhealthy
</code></pre>
<p>With a Circuit Breaker:</p>
<pre><code class="language-plaintext">1,000 requests
↓
Circuit OPEN
↓
1,000 fast failures
↓
Dependency protected
↓
Main service remains responsive
</code></pre>
<p>In both cases, there are failures. But the second scenario prevents those failures from consuming resources indefinitely. That’s the key distinction.</p>
<blockquote>
<p>The goal of a Circuit Breaker isn’t to make failure disappear. It’s to turn an uncontrolled failure into a controlled one.</p>
</blockquote>
<p>And that is one of the fundamental ideas behind resilient <a href="https://en.wikipedia.org/wiki/Distributed_computing">distributed systems</a>.</p>
<h2><strong>The State Machine in One Picture</strong></h2>
<p>Let’s summarize the state transitions:</p>
<p><strong>CLOSED</strong>: Requests are allowed through.</p>
<pre><code class="language-plaintext">CLOSED
↓
Requests flow normally
</code></pre>
<p>If failures exceed the configured threshold:</p>
<pre><code class="language-plaintext">CLOSED → OPEN
</code></pre>
<p><strong>OPEN:</strong> Requests fail fast without calling the dependency.</p>
<pre><code class="language-plaintext">OPEN
↓
Fail Fast
</code></pre>
<p>After the configured recovery timeout:</p>
<pre><code class="language-plaintext">OPEN → HALF-OPEN
</code></pre>
<p><strong>HALF-OPEN:</strong> A limited number of test requests are allowed.</p>
<p>If recovery succeeds:</p>
<pre><code class="language-plaintext">HALF-OPEN → CLOSED
</code></pre>
<p>If the dependency fails again:</p>
<pre><code class="language-plaintext">HALF-OPEN → OPEN
</code></pre>
<p>So the complete lifecycle is:</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/mtezleuhmqkm7ex57anz.png" alt="Image description" style="display:block;margin:0 auto" />

<h6>Image by geeksforgeeks</h6>
<h2><strong>The Bigger Lesson</strong></h2>
<p>It’s useful to remember the three states, But understanding <strong>why</strong> they exist is much more important.</p>
<p>When a dependency is failing, continuing to send more requests isn’t necessarily resilience. Sometimes the most resilient thing your system can do is <strong>stop making the problem worse</strong>.</p>
<blockquote>
<p>Circuit Breaker, together with carefully designed timeouts, limited retries, exponential backoff, jitter, fallback strategies, and idempotency, can help prevent a localized dependency failure from turning into a much larger outage.</p>
</blockquote>
<p>The central idea is simple:</p>
<blockquote>
<p><strong>When a dependency is unhealthy, protect your system first — and give the dependency a controlled chance to recover.</strong></p>
</blockquote>
<h2><strong>Final Takeaway</strong></h2>
<p>The Circuit Breaker pattern is a resilience mechanism for systems that depend on remote services or external resources where failures, timeouts, and temporary unavailability are possible.</p>
<p>Its three primary states are:</p>
<pre><code class="language-plaintext">CLOSED
↓
Requests flow normally
</code></pre>
<pre><code class="language-plaintext">OPEN 
↓ 
Requests fail fast
</code></pre>
<pre><code class="language-plaintext">HALF-OPEN
↓
A limited number of requests test recovery
</code></pre>
<p>The most important lesson isn’t memorizing these states. It’s understanding the philosophy behind them:</p>
<blockquote>
<p>A resilient system doesn’t just know how to handle failure. It knows when to stop making a failure worse.</p>
</blockquote>
<h2><strong>A Final Thought Experiment</strong></h2>
<p>Imagine your Payment Service is completely down. Your system receives <strong>10,000 payment requests per minute</strong>. Which behavior makes more sense ?</p>
<p><strong>Option 1:</strong> Retry every request three times. <strong>Option 2:</strong> Open the Circuit Breaker after the failure rate crosses the configured threshold and fail fast. <strong>Option 3:</strong> Use a combination of:</p>
<p>The interesting part isn’t memorizing which pattern to use. The real engineering challenge is understanding <strong>how these mechanisms interact under failure</strong>. That’s where resilient distributed-system design begins.</p>
<h3><strong>Further Reading</strong></h3>
<ul>
<li><p><a href="https://learn.microsoft.com/ar-sa/azure/architecture/patterns/circuit-breaker">Microsoft Azure — Circuit Breaker Pattern</a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/architecture/best-practices/transient-faults">Microsoft Azure — Transient Fault Handling</a></p>
</li>
<li><p><a href="https://learn.microsoft.com/uk-ua/azure/architecture/antipatterns/retry-storm">Microsoft Azure — Retry Storm Antipattern</a></p>
</li>
<li><p><a href="https://aws.amazon.com/blogs/compute/using-the-circuit-breaker-pattern-with-aws-step-functions-and-amazon-dynamodb/">AWS — Using the Circuit Breaker Pattern with Step Functions and DynamoDB</a></p>
</li>
<li><p><a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs">AWS Builders’ Library — Making retries safe with idempotent APIs</a></p>
</li>
<li><p><a href="https://martinfowler.com/bliki/CircuitBreaker.html">Martin Fowler — Circuit Breaker</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern">Circuit Breaker Design Pattern</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/resilient-distributed-systems/">Resilient Distributed Systems</a></p>
</li>
<li><p><a href="https://blog.bytebytego.com/p/top-strategies-to-improve-reliability">Top Strategies to Improve Reliability in Distributed Systems</a></p>
</li>
<li><p><a href="https://medium.com/@ninadwalanj/how-failure-cascades-in-distributed-systems-eccc48c9851a">How failure cascades in Distributed Systems</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/operating-systems/recovery-in-distributed-systems/">Recovery in Distributed Systems</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/monolithic-architecture-system-design/">Monolithic Architecture</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Finite-state_machine">Finite-State Machine</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Service-level_objective">Service-Level Objective</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/retries-strategies-in-distributed-systems/">Retries Strategies in Distributed Systems</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/computer-networks/failure-detection-and-recovery-in-distributed-systems/">Failure Detection and Recovery in Distributed Systems</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/graceful-degradation-in-distributed-systems/">Graceful Degradation in Distributed Systems</a></p>
</li>
<li><p><a href="https://docs.oracle.com/en/database/other-databases/timesten/22.1/scaleout/recovering-transient-errors.html#GUID-B8CC179C-C56A-4994-9607-D1DB2EB5F32F">Recovering from Transient Errors</a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/architecture/antipatterns/retry-storm/">Retry Storm Antipattern</a></p>
</li>
<li><p><a href="https://dilankam.medium.com/understanding-retries-exponential-backoffs-and-circuit-breakers-in-distributed-systems-4355db103505">Understanding Retries, Exponential Backoffs, and Circuit Breakers in Distributed Systems</a></p>
</li>
<li><p><a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/">Exponential Backoff And Jitter</a></p>
</li>
<li><p><a href="https://www.testdevlab.com/blog/false-positives-and-negatives-in-software-testing">What Are False Positives and Negatives in Software Testing ?</a></p>
</li>
<li><p><a href="https://aws.amazon.com/what-is/service-mesh/">What is a Service Mesh ?</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/sidecar-design-pattern-for-microservices/">Sidecar Design Pattern for Microservices</a></p>
</li>
<li><p><a href="https://medium.com/@AI-Simplified/distributed-systems-and-microservices-guide-to-streamlined-software-architectures-2782673e5808">Distributed Systems and Microservices: Guide to Streamline Software Architectures</a></p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/bulkhead">Bulkhead Pattern</a></p>
</li>
<li><p><a href="https://dev.to/smiah/rate-limiting-in-distributed-system-3h59">Rate Limiting in Distributed System</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/distributed-task-queue-distributed-systems/">Distributed Task Queue — Distributed Systems</a></p>
</li>
<li><p><a href="https://copyconstruct.medium.com/health-checks-in-distributed-systems-aa8a0e8c1672">Health Checks and Graceful Degradation in Distributed Systems</a></p>
</li>
<li><p><a href="https://www.geeksforgeeks.org/system-design/observability-in-distributed-systems/">Observability in Distributed Systems</a></p>
</li>
<li><p><a href="https://dzone.com/articles/importance-of-idempotency-in-distributed-systems">Idempotency in Distributed Systems: When and Why It Matters</a></p>
</li>
<li><p><a href="https://en.wikipedia.org/wiki/Distributed_computing">Distributed Computing</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[MySQL Overselling: Why SELECT Isn't Enough and How SELECT ... FOR UPDATE Solves It]]></title><description><![CDATA[Imagine you’re running an online store, there’s exactly one item left in stock. Two customers click Buy at almost exactly the same time. Both requests reach your application. Both transactions ask the]]></description><link>https://mehradsadeghi.hashnode.dev/mysql-select-and-select-for-update</link><guid isPermaLink="true">https://mehradsadeghi.hashnode.dev/mysql-select-and-select-for-update</guid><category><![CDATA[database]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[backend]]></category><category><![CDATA[#softwareengineering]]></category><dc:creator><![CDATA[Mehrad]]></dc:creator><pubDate>Sun, 13 Sep 2026 14:57:37 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/afa4ffbc-64f1-4811-91e9-fb843e0e2f0d.jpg" alt="" style="display:block;margin:0 auto" />

<p>Imagine you’re running an online store, there’s exactly <strong>one item left</strong> in stock. Two customers click <strong>Buy</strong> at almost exactly the same time. Both requests reach your application. Both transactions ask the database:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>And both get:</p>
<pre><code class="language-python">stock = 1
</code></pre>
<p>Now both customers believe the product is available.</p>
<p><strong>So what happens next ? Welcome to the overselling problem.</strong></p>
<h3>The Naive Implementation</h3>
<p>A simple purchase flow might look like this:</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT stock FROM products WHERE id = 10;
-- Application checks:
-- Is stock &gt; 0 ?
UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO orders (product_id, user_id) VALUES (10, 1001);
COMMIT;
</code></pre>
<p>At first glance, this seems perfectly reasonable. But there’s a race condition.</p>
<p>Imagine two transactions:</p>
<pre><code class="language-plaintext">Transaction A              Transaction B

SELECT stock               SELECT stock
     |                           |
     v                           v
   stock=1                    stock=1
     |                           |
     v                           v
"Available!"                 "Available!"
</code></pre>
<p>Both transactions read the same value before either one has completed the purchase. The problem isn’t necessarily that MySQL is broken. The problem is that our <strong>read and decision are not protected as one atomic operation</strong>.</p>
<h3>Why Doesn’t REPEATABLE READ Solve This ?</h3>
<p>This is an important question. InnoDB’s default isolation level is REPEATABLE READ. So you might think:</p>
<blockquote>
<p>“If I’m using REPEATABLE READ, shouldn’t MySQL prevent this ?”</p>
</blockquote>
<p><strong>Not necessarily</strong>.</p>
<p>A normal:</p>
<pre><code class="language-sql">SELECT ...
</code></pre>
<p>is a consistent, nonlocking read.</p>
<p>Under <code>REPEATABLE READ</code>, it can read from the transaction's consistent snapshot.</p>
<p>Isolation determines what your transaction sees. It doesn’t automatically mean:</p>
<blockquote>
<p>“Nobody else can modify the row after I read it.”</p>
</blockquote>
<p>That’s a completely different requirement.</p>
<p>If your business operation is:</p>
<blockquote>
<p>“Read this row, verify a condition, and then modify it.”</p>
</blockquote>
<p>you often need a <strong>locking read</strong>.</p>
<h3>Enter SELECT … FOR UPDATE</h3>
<p>MySQL provides:</p>
<pre><code class="language-sql">SELECT ... FOR UPDATE
</code></pre>
<p>For example:</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>This is not just a normal read. It is a <strong>locking read</strong>.</p>
<p>MySQL/InnoDB locks the records returned by the query, and another transaction attempting to acquire a conflicting lock on the same records has to wait until the first transaction commits or rolls back.</p>
<p>Now our two transactions look very different.</p>
<h3>Transaction A Gets There First</h3>
<p>Suppose Transaction A executes:</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>The database finds:</p>
<pre><code class="language-python">stock = 1
</code></pre>
<p>and locks the relevant record.</p>
<p>Transaction B now tries:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>But Transaction A is already holding the conflicting lock.</p>
<p>So Transaction B waits.</p>
<pre><code class="language-plaintext">Transaction A                 Transaction B
FOR UPDATE
     |
     v
 stock = 1
     |
  LOCK ROW
     |
     |                       FOR UPDATE
     |                            |
     |                            v
     |                         WAIT...
     |
 UPDATE stock = 0
     |
 INSERT ORDER
     |
   COMMIT
     |
  UNLOCK
                                |
                                v
                          FOR UPDATE succeeds
</code></pre>
<p>Now Transaction B gets its turn.</p>
<p>The important part is that <strong>Transaction B doesn’t get to make its decision before Transaction A finishes</strong>.</p>
<h3>The Correct Purchase Flow</h3>
<p>A safer implementation looks like this:</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>Then the application checks the returned value:</p>
<pre><code class="language-python">if stock &gt; 0:
    continue purchase
else:
    reject purchase
</code></pre>
<p>If stock is available:</p>
<pre><code class="language-sql">UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO orders (product_id, user_id) VALUES (10, 1001);
COMMIT;
</code></pre>
<p>If the product is already sold out:</p>
<pre><code class="language-sql">ROLLBACK;
</code></pre>
<p>The important property is that the <strong>check and subsequent modification happen while the relevant row is locked</strong>.</p>
<h3>The Key Idea: Lock Before You Decide</h3>
<p>This is the mental model worth remembering:</p>
<p><strong>Unsafe</strong>:</p>
<pre><code class="language-plaintext">READ
  ↓
CHECK
  ↓
UPDATE
</code></pre>
<p>The problem is that another transaction can interfere between the read and the update.</p>
<p><strong>Safer</strong>:</p>
<pre><code class="language-plaintext">LOCK + READ
     ↓
   CHECK
     ↓
   UPDATE
     ↓
   COMMIT
</code></pre>
<p>The lock protects the critical section.</p>
<h3>But There’s Another Option</h3>
<p>For a simple inventory decrement, you don’t always need to read the row first. You can make the condition part of the update itself:</p>
<pre><code class="language-sql">UPDATE products SET stock = stock - 1 WHERE id = 10 AND stock &gt; 0;
</code></pre>
<p>Then check how many rows were affected.</p>
<p>If:</p>
<pre><code class="language-python">affected_rows = 1
</code></pre>
<p>the purchase can proceed.</p>
<p>If:</p>
<pre><code class="language-python">affected_rows = 0
</code></pre>
<p>there wasn’t enough stock.</p>
<p>This approach can be extremely useful because the business condition is enforced directly by the database operation.</p>
<p>For example:</p>
<pre><code class="language-sql">START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE id = 10 AND stock &gt; 0;
-- If one row was updated:
-- create the order
INSERT INTO orders (product_id, user_id) VALUES (10, 1001);
COMMIT;
</code></pre>
<p>In a real implementation, the application should only insert the order when the update actually succeeded.</p>
<h3>So When Should You Use SELECT … FOR UPDATE ?</h3>
<p><code>SELECT ... FOR UPDATE</code> is particularly useful when your business logic needs to:</p>
<ol>
<li><p>Read the current state of a row.</p>
</li>
<li><p>Make a decision based on that state.</p>
</li>
<li><p>Modify that same data.</p>
</li>
<li><p>Keep another transaction from changing it between those steps.</p>
</li>
</ol>
<p>Common examples include:</p>
<ul>
<li><p>inventory reservation</p>
</li>
<li><p>seat reservation</p>
</li>
<li><p>wallet balance updates</p>
</li>
<li><p>account transfers</p>
</li>
<li><p>job claiming</p>
</li>
<li><p>resource allocation</p>
</li>
<li><p>order processing</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-sql">SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
</code></pre>
<p>Then:</p>
<pre><code class="language-plaintext">Check balance
     ↓
Calculate new balance
     ↓
UPDATE account
     ↓
COMMIT
</code></pre>
<p>The lock protects the critical decision.</p>
<h3>A Common Misunderstanding</h3>
<p>One common misconception is:</p>
<blockquote>
<p>“FOR UPDATE locks the entire table."</p>
</blockquote>
<p>That’s not generally how InnoDB works.</p>
<p>InnoDB uses row-level locking, although the exact locks acquired depend on the query, indexes, search conditions, and isolation level. For some range queries, InnoDB can also use gap locks or next-key locks.</p>
<p>For example, a unique-index lookup such as:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>can lock the matching index record.</p>
<p>Range-based locking is more complicated.</p>
<p>For example:</p>
<pre><code class="language-sql">SELECT * FROM products WHERE price BETWEEN 100 AND 200 FOR UPDATE;
</code></pre>
<p>may involve range-related locking behavior depending on the indexes and isolation level.</p>
<p>This is one reason understanding indexes is important when reasoning about MySQL concurrency.</p>
<h3>Don’t Forget the Transaction</h3>
<p><code>FOR UPDATE</code> makes sense inside a transaction.</p>
<p>For example:</p>
<pre><code class="language-sql">START TRANSACTION;
SELECT stock FROM products WHERE id = 10 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO orders (product_id, user_id) VALUES (10, 1001);
COMMIT;
</code></pre>
<p>The lock is part of the transaction’s concurrency control. You don’t want to acquire a lock, perform a tiny operation, release it, and then perform the critical business operation later. The entire critical section should be designed intentionally.</p>
<h3>What About Deadlocks ?</h3>
<p>Locks solve one class of concurrency problems, but they introduce another possibility: <strong>deadlocks</strong>.</p>
<p>Imagine:</p>
<pre><code class="language-plaintext">Transaction A                 Transaction B
locks Row 1                   locks Row 2
     |                              |
     v                              v
tries Row 2                    tries Row 1
     |                              |
     +---------- WAIT &lt;-------------+
</code></pre>
<p>Now both transactions are waiting for each other.</p>
<p>InnoDB detects deadlocks and rolls back one of the transactions so that the other can continue.</p>
<p>This means production applications using transactions and locks should generally be prepared to <strong>retry transactions when appropriate</strong>.</p>
<p>Locking isn’t something you simply add without considering transaction boundaries, lock ordering, indexes, and failure handling.</p>
<h3>The Real Lesson</h3>
<p>The important lesson isn’t simply:</p>
<blockquote>
<p>“Use SELECT ... FOR UPDATE."</p>
</blockquote>
<p>The deeper lesson is:</p>
<blockquote>
<p>Concurrency bugs happen when a business decision depends on data that can change between the read and the write.</p>
</blockquote>
<p>You need to identify that critical section and choose an appropriate concurrency-control strategy.</p>
<p>Sometimes that’s:</p>
<pre><code class="language-sql">SELECT ... FOR UPDATE
</code></pre>
<p>Sometimes it’s an atomic conditional update:</p>
<pre><code class="language-sql">UPDATE ... WHERE stock &gt; 0;
</code></pre>
<p>Sometimes it’s an optimistic concurrency strategy. And sometimes a database constraint is the best solution. The correct choice depends on the business operation.</p>
<h3>Final Takeaway</h3>
<p>When building a system that handles concurrent requests, don’t ask only:</p>
<blockquote>
<p>“Which isolation level should I use ?”</p>
</blockquote>
<p>Also ask:</p>
<blockquote>
<p>“What happens if two transactions execute these exact statements at the same time ?”</p>
</blockquote>
<p>That’s the question that exposes race conditions.</p>
<p>For an inventory operation, this:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>and this:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>are not equivalent.</p>
<p>The first reads. The second <strong>reads with an intention to modify and acquires a lock</strong>.</p>
<p>Once you understand that distinction, MySQL transaction isolation becomes much easier to reason about.</p>
<p>And more importantly, you can start designing systems that remain correct even when thousands of users click <strong>Buy</strong> at exactly the same time.</p>
]]></content:encoded></item><item><title><![CDATA[Two Transactions, One Row: What Does MySQL Actually Let You See ?]]></title><description><![CDATA[Can one transaction see another transaction’s uncommitted changes ? Can the same query return different results inside a single transaction ? And why does SELECT sometimes give you a snapshot, while S]]></description><link>https://mehradsadeghi.hashnode.dev/mysql-innodb-transaction-isolation-levels</link><guid isPermaLink="true">https://mehradsadeghi.hashnode.dev/mysql-innodb-transaction-isolation-levels</guid><category><![CDATA[database]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[transaction]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[#softwareengineering]]></category><dc:creator><![CDATA[Mehrad]]></dc:creator><pubDate>Sun, 13 Sep 2026 13:32:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa6a22601d929592ef631fa/7fc38a7b-7e2c-4934-8862-771de1ded3fc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Can one transaction see another transaction’s uncommitted changes ? Can the same query return different results inside a single transaction ? And why does <code>SELECT</code> sometimes give you a snapshot, while <code>SELECT ... FOR UPDATE</code> gives you something completely different ?</p>
<p>If you’ve ever wondered what MySQL actually does when transactions run concurrently, this is where it gets interesting.</p>
<p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html">InnoDB</a> gives you four <a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)">transaction isolation levels</a>:</p>
<ul>
<li><p>READ UNCOMMITTED</p>
</li>
<li><p>READ COMMITTED</p>
</li>
<li><p>REPEATABLE READ</p>
</li>
<li><p>SERIALIZABLE</p>
</li>
</ul>
<p>Most developers know their names, fewer can predict what will actually happen when two transactions execute at the same time.</p>
<p>That’s what we’re going to do here.</p>
<p><strong>No abstract definitions first. No memorizing a table.</strong> We’ll make the transactions collide and see what MySQL does.</p>
<h2>Why Do We Need Transactions ?</h2>
<p>Consider a typical e-commerce purchase. When a customer buys a product, several database operations may need to happen together:</p>
<ol>
<li><p>Check the product inventory.</p>
</li>
<li><p>Decrease the inventory.</p>
</li>
<li><p>Create the order.</p>
</li>
<li><p>Record the payment.</p>
</li>
<li><p>Update the order status.</p>
</li>
</ol>
<p>What happens if the inventory is successfully decreased but creating the order fails ?</p>
<p>You could end up with a database where the product’s inventory has decreased, but no corresponding order exists.</p>
<p>That’s exactly the kind of problem transactions are designed to prevent.</p>
<p>A transaction lets us treat multiple database operations as a single logical unit:</p>
<p>Either all of the operations succeed, or the changes are rolled back.</p>
<p>For example:</p>
<pre><code class="language-sql">START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE id = 10;
INSERT INTO orders (product_id, user_id) VALUES (10, 1001);
COMMIT;
</code></pre>
<p>If something goes wrong:</p>
<pre><code class="language-sql">ROLLBACK;
</code></pre>
<p>Transactions are one of the foundations of <a href="https://en.wikipedia.org/wiki/ACID">ACID</a>, and the I in ACID stands for Isolation. Isolation is where things get interesting.</p>
<h2>What Is a Transaction Isolation Level ?</h2>
<p>Imagine two transactions running at approximately the same time:</p>
<pre><code class="language-plaintext">Transaction A                Transaction B
      |                            |
      |---- read data ------------&gt;|
      |                            |
      |                       update data
      |                            |
      |&lt;--- what can A see ? ------|
</code></pre>
<p>Both transactions may be reading and modifying the same data. The isolation level defines the rules for what each transaction is allowed to see while other transactions are running.</p>
<p>Those rules can dramatically change the behavior of your application.</p>
<p>For example:</p>
<ul>
<li><p>Can you see another transaction’s uncommitted changes ?</p>
</li>
<li><p>Can the same query return a different value later ?</p>
</li>
<li><p>Do you keep seeing the same snapshot ?</p>
</li>
<li><p>Does your read lock the row ?</p>
</li>
<li><p>Can another transactions modify the row while you’re making a decision ?</p>
</li>
</ul>
<p>Let’s find out.</p>
<h3>1. READ UNCOMMITTED - When Uncommitted Data Becomes Visible</h3>
<p>READ UNCOMMITTED is the least restrictive isolation level. A transaction can potentially see changes made by another transaction before those changes have been committed. This is known as a dirty read.</p>
<p>Suppose the database contains:</p>
<pre><code class="language-python">stock = 10
</code></pre>
<p>Transaction A starts:</p>
<pre><code class="language-sql">START TRANSACTION;
UPDATE products SET stock = 0 WHERE id = 10;
</code></pre>
<p>But Transaction A hasn’t committed yet.</p>
<p>Now Transaction B executes:</p>
<pre><code class="language-sql">SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>Transaction B may see:</p>
<pre><code class="language-plaintext">0
</code></pre>
<p>Wait. That 0 hasn't actually been committed.</p>
<p>Now Transaction A rolls everything back:</p>
<pre><code class="language-sql">ROLLBACK;
</code></pre>
<p>The actual value goes back to:</p>
<pre><code class="language-plaintext">10
</code></pre>
<p>Transaction B just observed a value that never became part of the committed database state. That’s a dirty read.</p>
<h4>Why is this dangerous ?</h4>
<p>Imagine applying the same idea to:</p>
<ul>
<li><p>payments</p>
</li>
<li><p>bank balances</p>
</li>
<li><p>inventory</p>
</li>
<li><p>orders</p>
</li>
<li><p>seat reservations</p>
</li>
</ul>
<p>You could make a business decision based on data that ultimately disappears. A simple way to remember it:</p>
<blockquote>
<p>READ UNCOMMITTED: you get more concurrency by giving up consistency.</p>
</blockquote>
<h3>2. READ COMMITTED - Only See What Has Been Committed</h3>
<p>READ COMMITTED takes a more conservative approach. A transaction doesn’t see another transaction’s uncommitted changes through a normal consistent read. But there’s a catch.</p>
<p>The data you see can change while your transaction is still running.</p>
<p>Suppose:</p>
<pre><code class="language-plaintext">Initial stock = 10
</code></pre>
<p>Transaction A starts:</p>
<pre><code class="language-sql">SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>It sees:</p>
<pre><code class="language-plaintext">10
</code></pre>
<p>Now Transaction B changes the value:</p>
<pre><code class="language-sql">START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 10;
COMMIT;
</code></pre>
<p>Transaction A executes the same query again:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>This time:</p>
<pre><code class="language-plaintext">5
</code></pre>
<p>The same transaction saw 10 and later 5. That’s a Non-Repeatable Read.</p>
<p>But why this happens ? Because under READ COMMITTED, each consistent read can establish its own fresh snapshot.</p>
<h4>When is READ COMMITTED useful ?</h4>
<p>It’s a good fit for systems where seeing relatively fresh committed data is more important than maintaining one consistent snapshot throughout the entire transaction.</p>
<p>It also changes InnoDB’s locking behavior. For locking READ, UPDATE, and DELETE, InnoDB generally uses record locks rather than gap locks, except where gap locking is needed for foreign-key and duplicate-key checks.</p>
<p>A simple way to remember it:</p>
<blockquote>
<p>READ COMMITTED: every consistent read sees committed data as of that read, so a later read can see changes committed by other transactions.</p>
</blockquote>
<h3>3. REPEATABLE READ - Your Transaction Gets a Snapshot</h3>
<p>Now we reach InnoDB’s default isolation level: REPEATABLE READ.</p>
<p>Suppose:</p>
<pre><code class="language-python">stock = 10
</code></pre>
<p>Transaction A starts:</p>
<pre><code class="language-sql">SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>It sees:</p>
<pre><code class="language-plaintext">10
</code></pre>
<p>Now Transaction B changes the value:</p>
<pre><code class="language-sql">START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 10;
COMMIT;
</code></pre>
<p>Transaction A runs the same query again:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>And for a normal, non-locking consistent read, it can still see <strong>10</strong>, not <strong>5</strong>.</p>
<p>Why is that ? Because consistent reads in a REPEATABLE READ transaction use the transaction's snapshot.</p>
<p>So from Transaction A’s perspective, the database can effectively look like:</p>
<p>Transaction A’s view:</p>
<pre><code class="language-python">stock = 10
</code></pre>
<p>even though the current committed value is now:</p>
<pre><code class="language-python">stock = 5
</code></pre>
<h3>4. SERIALIZABLE - Make Concurrency Behave More Like Sequential Execution</h3>
<p>SERIALIZABLE is the strictest of the four isolation levels. Its goal is to provide behavior that is closer to transactions executing one after another rather than freely interleaving.</p>
<p>Conceptually:</p>
<pre><code class="language-plaintext">Transaction A
     |
     | read/write
     |
     v
Transaction B
     |
     | waits
     v
Transaction A commits
     |
     v
Transaction B continues
</code></pre>
<p>In InnoDB, when auto-commit is disabled, plain <code>SELECT</code> statements under SERIALIZABLE are implicitly converted to locking reads using <code>FOR SHARE</code>.</p>
<p>This provides stronger consistency guarantees, but the additional locking can reduce concurrency.</p>
<p>So while <code>SERIALIZABLE</code> sounds like the safest choice, it isn't automatically the best choice. Stronger isolation comes with a cost.</p>
<p>A simple way to remember it:</p>
<blockquote>
<p>SERIALIZABLE: strongest isolation, but concurrency becomes more expensive.</p>
</blockquote>
<p>Comparing the Four Levels</p>
<p>Here’s the quick mental model:</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/at0gar03l0lewjvvxt45.png" alt="Image description" style="display:block;margin:0 auto" />

<p>But don’t treat this table as the whole story.</p>
<p>InnoDB’s behavior also involves:</p>
<ul>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-multi-versioning.html">MVCC</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/9.7/en/innodb-consistent-read.html">consistent reads</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html">locking reads</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html">record locks</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html">gap locks</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html">next-key locks</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/9.7/en/innodb-transaction-model.html">transaction boundaries</a></p>
</li>
<li><p><a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html">indexes</a></p>
</li>
</ul>
<p>The interesting part isn’t memorizing the table. <strong>It’s being able to predict what happens when two transactions collide</strong>.</p>
<h3>The Bigger Lesson</h3>
<p>Here’s the part that matters most in real applications:</p>
<p>Choosing an isolation level doesn’t automatically make your concurrent code correct.</p>
<p>Isolation level determines the visibility and concurrency rules of transactions, But your SQL statements determine how you interact with the data.</p>
<p>Compare:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10;
</code></pre>
<p>with:</p>
<pre><code class="language-sql">SELECT stock FROM products WHERE id = 10 FOR UPDATE;
</code></pre>
<p>They may look almost identical, but they are not.</p>
<p>The first is a consistent read.</p>
<p>The second is a locking read.</p>
<p>And when two requests hit your application at almost exactly the same time, that difference can determine whether your system behaves correctly.</p>
<h3>The Question You Should Ask</h3>
<p>When debugging or designing concurrent database code, don’t ask only:</p>
<p><strong>“Which isolation level am I using ?”</strong></p>
<p>Ask this instead:</p>
<p><strong>“What happens if two transactions execute these exact statements at the same time ?”</strong></p>
<p>That’s the question that exposes race conditions.</p>
<p>Once you start thinking in terms of concurrent transactions instead of isolated SQL statements, MySQL’s transaction model becomes much easier to understand.</p>
<h3>Final Takeaway</h3>
<p>Transaction isolation isn’t just a list of four configuration values. It’s a set of rules governing what your transactions can see, when they can see it, and how they interact with other transactions.</p>
<p>If you remember only one thing from this article, remember this:</p>
<p>The real test of your database design isn’t what happens when one transaction runs. It’s what happens when two transactions run at the same time.</p>
<p>And when those two transactions are fighting over the last item in stock, the difference between a normal <code>SELECT</code> and <code>SELECT ... FOR UPDATE</code> suddenly becomes very important.</p>
<p>That’s where we’re going next.</p>
<h3>Next: <a href="https://mehradsadeghi.hashnode.dev/mysql-select-and-select-for-update">MySQL Overselling - Why <code>SELECT</code> Isn't Enough and How <code>SELECT ... FOR UPDATE</code> Solves It</a></h3>
]]></content:encoded></item></channel></rss>