<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>changelog</title>
    <link>https://changelog.encer.me/en/</link>
    <description>Notes on building products, AI, and software — for a global developer audience.</description>
    <language>en</language>
    <atom:link href="https://changelog.encer.me/en/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>When only one unit remains, which order gets it?</title>
      <link>https://changelog.encer.me/en/posts/last-item-inventory-reservation</link>
      <guid isPermaLink="true">https://changelog.encer.me/en/posts/last-item-inventory-reservation</guid>
      <pubDate>Tue, 27 Oct 2026 00:00:00 GMT</pubDate>
      <description>How to handle competing orders, failed payments, abandoned checkouts, and uncertain payment outcomes when reserving inventory.</description>
      <category>commerce</category>
      <category>inventory</category>
      <category>payment</category>
      <category>concurrency</category>
      <category>reliability</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> For the last unit, do not authorize payment first—hold inventory for one order, show sold out to everyone else, then request payment authorization. Model <code>available → reserved for checkout → sold</code> (release on clear failure, expiry on abandonment, hold through uncertain payment outcomes). Use a single conditional <code>UPDATE</code> to claim stock, idempotent release, and treat payment timeouts as <em>unknown</em> until the processor confirms success or failure.</p>
<h1 id="when-only-one-unit-remains-which-order-gets-it">When only one unit remains, which order gets it?<a class="heading-anchor" href="#when-only-one-unit-remains-which-order-gets-it" aria-label="이 섹션으로 가는 링크">#</a></h1>
<p>Two people place orders for a product that has one unit left, almost at the same time.</p>
<p>If both reach payment, both see “your order is being processed,” and only then does someone get a cancellation notice, the problem is not a wrong count of one. You trigger payment reversals, refunds, support load, seller firefighting, and lost trust.</p>
<p>The principle I want is simple:</p>
<blockquote><p>Only one order should get the last unit. For everyone else, sold out beats a payment reversal.</p></blockquote>
<p>So when stock is down to one, I do not authorize payment first. I hold inventory for a single order and show sold out to others. Then I request payment authorization. On a clear payment failure, release the hold immediately; on checkout abandonment, release only after a defined TTL.</p>
<p>The point is not the slogan “deduct inventory first.” <strong>Treat inventory as reserved and define who can confirm or roll back that reservation under each outcome.</strong></p>
<h2 id="between-deduction-and-payment-authorization-there-is-a-reservation-state">Between deduction and payment authorization there is a reservation state<a class="heading-anchor" href="#between-deduction-and-payment-authorization-there-is-a-reservation-state" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>If you decrement stock before taking payment, someone will ask:</p>
<blockquote><p>When payment fails, how do you put inventory back?</p></blockquote>
<p>The answer is reservation, not a permanent decrement. The moment you assign the last unit to an order, the state should be <strong>reserved for checkout</strong>, not sold. (These labels are illustrative—not tied to a particular product codebase.)</p>
<ul><li><strong>Available for sale:</strong> New orders may claim a hold.</li><li><strong>Reserved for checkout:</strong> Only that order may complete payment; others see sold out.</li><li><strong>Sold:</strong> Payment is final; no further sales.</li><li><strong>Released:</strong> After failure, cancel, or expiry, stock returns to available for sale.</li></ul>
<p>Stripe Checkout for limited inventory uses session expiry so unpaid customers do not hold stock indefinitely, and documents listening for expiry to restore reserved quantity.[1] Session TTL is not the universal answer for every payment method or custom checkout—set hold duration from time-to-pay, scarcity, and processor behavior.</p>
<p>UX follows the same rule: if the last unit is reserved for checkout, do not tease other customers with “almost yours.” Show sold out until the hold expires.</p>
<h2 id="claiming-one-order-breaks-if-you-split-read-and-write">Claiming one order breaks if you split read and write<a class="heading-anchor" href="#claiming-one-order-breaks-if-you-split-read-and-write" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>The common mistake is read stock, decide in application code that stock exists, then decrement later.</p>
<pre><code class="language-text">1. Read current stock: 1
2. If stock &gt; 0, create the order
3. Subtract 1 from stock</code></pre>
<p>Two requests can both read 1 at step 1 and both create orders. The bug is not the payment provider—it is separating the inventory decision from the write.</p>
<p>Claiming the last unit must put condition and mutation in one database write, for example:</p>
<pre><code class="language-sql"><span class="hl-keyword">UPDATE</span> inventory
<span class="hl-keyword">SET</span> available_quantity = available_quantity - <span class="hl-number">1</span>,
    reserved_quantity = reserved_quantity + <span class="hl-number">1</span>
<span class="hl-keyword">WHERE</span> product_id = :product_id
  <span class="hl-keyword">AND</span> available_quantity &gt; <span class="hl-number">0</span>
RETURNING available_quantity;</code></pre>
<p><code>UPDATE</code> changes only rows that satisfy <code>WHERE</code>; <code>RETURNING</code> gives back the new values.[3] No returned row means this request did not claim the last unit. Under PostgreSQL’s default Read Committed, concurrent updaters on the same row wait for the first commit, then re-evaluate <code>WHERE</code> against the updated row.[7]</p>
<p>That beats <code>SELECT</code> then “we still have stock, so decrement.” Multi-SKU carts, coupons, limits, and split carts may need transactions, lock ordering, or optimistic concurrency—not a single SQL generalization for every flow. Row locks block other writers until the transaction ends.[8]</p>
<h2 id="payment-failure-abandonment-and-timeout-are-not-the-same-failure">Payment failure, abandonment, and timeout are not the same failure<a class="heading-anchor" href="#payment-failure-abandonment-and-timeout-are-not-the-same-failure" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>“Release the hold” does not mean one code path for every bad outcome.</p>
<h3 id="1-clear-payment-failure-recover-immediately">1. Clear payment failure → recover immediately<a class="heading-anchor" href="#1-clear-payment-failure-recover-immediately" aria-label="이 섹션으로 가는 링크">#</a></h3>
<p>On an explicit decline (e.g. card rejected), release that order’s reservation at once. Release means <strong>only while that reservation is still active</strong>—not blindly adding 1 to stock.</p>
<p>Track <code>reservation_id</code> and states so <code>RESERVED → RELEASED</code> happens once. Duplicate failure webhooks or manual replays must not inflate inventory twice.</p>
<h3 id="2-checkout-abandonment-recover-via-expiry">2. Checkout abandonment → recover via expiry<a class="heading-anchor" href="#2-checkout-abandonment-recover-via-expiry" aria-label="이 섹션으로 가는 링크">#</a></h3>
<p>Closing the browser does not always emit a failure event. Reservations need a server-side expiry time. After expiry, find the hold, release it, and refresh sold-out UI to available if appropriate.</p>
<p>Client countdowns are guidance only; the server owns expiry and state. Stripe describes restoring reserved inventory on <code>checkout.session.expired</code> after session expiry.[1]</p>
<h3 id="3-timeout-unknown-outcome-not-immediate-failure">3. Timeout → unknown outcome, not immediate failure<a class="heading-anchor" href="#3-timeout-unknown-outcome-not-immediate-failure" aria-label="이 섹션으로 가는 링크">#</a></h3>
<p>The dangerous case: you sent a payment request and got no response. A network timeout does not mean the processor never received the request—it may have approved and the response is late. Treat external call timeouts as <strong>unknown</strong> until reconciled; without idempotency keys or inquiry APIs from the processor, you choose between duplicate charges and lost approvals.</p>
<p>Do not release inventory the instant a timeout fires. “Recover after the timeout window” here means:</p>
<ol><li>Define per-processor max response and webhook delay in operations policy.</li><li>Until then, keep the reservation in an <strong>awaiting payment outcome</strong> state (payment may have succeeded, failed, or still be in flight—you do not know yet).</li><li>After the deadline, query the processor and internal records; release only if there is no success.</li><li>When a late success webhook collides with release, use conditional transitions so one wins.</li></ol>
<p>That conservatism can briefly show false sold-out. It beats missing a successful charge or promising the last unit twice. If the processor offers no inquiry API or idempotency contract, this becomes a business risk choice, not purely engineering.</p>
<h2 id="retries-must-not-create-a-new-order">Retries must not create a new order<a class="heading-anchor" href="#retries-must-not-create-a-new-order" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Users double-tap pay; mobile networks drop responses. A repeated create-order request must return the first attempt’s outcome—not a new hold or new payment.</p>
<p>HTTP <code>POST</code> may apply state change on every call. An API can use a stable <code>Idempotency-Key</code> so the server treats replays as one operation—<strong>if it stores and enforces the key</strong>; support is API-specific.[4] Stripe returns the first result for the same key and errors when parameters differ under the same key.[5]</p>
<p>Do not treat the key as log decoration. Persist at least:</p>
<ul><li>Order or reservation ID for the key</li><li>Processing state (<code>processing</code>, <code>completed</code>, <code>failed</code>)</li><li>Fingerprint of the original input</li><li>Stored response and retention period</li></ul>
<p>In-flight duplicate keys should return “processing,” not a new order. Completed keys should replay the stored response. Idempotency is a <strong>contract to reuse the outcome of one business request</strong>, not magic in the header alone.</p>
<p>AWS recommends persisting idempotency tokens and related state and coordinating token records with state changes under proper concurrency control to avoid races.[6]</p>
<h2 id="do-not-try-one-db-transaction-for-inventory-and-the-payment-processor">Do not try one DB transaction for inventory and the payment processor<a class="heading-anchor" href="#do-not-try-one-db-transaction-for-inventory-and-the-payment-processor" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Inventory DB and an external processor cannot share one atomic transaction. You need explicit success and compensation per step, not “finish both at once.”</p>
<pre><code class="language-text">Available for sale
  → Create reservation / claim stock
  → Request payment
  → Payment success: confirm reservation / order complete
  → Explicit payment failure: release reservation
  → Customer leaves: release after expiry
  → Response timeout: payment pending → query/deadline → confirm or release</code></pre>
<p>That sequence of local changes and compensating steps is a saga. AWS distinguishes compensating transactions for business-rule failures (roll back inventory/order state) from forward recovery (retries) for infrastructure failures.[2]</p>
<p>If order confirmation triggers notifications, fulfillment, or analytics events, you can still die after committing inventory but before publish. Transactional outbox (same transaction for state + outbox row, relay publishes later) is worth considering at scale—but for a small order flow, get reservation / confirm / release transitions right first.</p>
<h2 id="counterpoint-won-t-holding-stock-before-payment-hurt-conversion">Counterpoint: Won’t holding stock before payment hurt conversion?<a class="heading-anchor" href="#counterpoint-won-t-holding-stock-before-payment-hurt-conversion" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Yes, sometimes. Long holds let non-payers block scarce SKUs while others only see sold out. Short holds on scarce items can feel pressuring.</p>
<p>This post is not “every shop needs long holds.”</p>
<ul><li>Plenty of stock: deduct after payment may stay simpler.</li><li>Last units, limited drops, seats/tickets: reservation often wins when oversell cost is high.</li><li>Hold duration should follow processor limits, real pay times, and the cost of cancellations—not marketing copy alone.</li><li>Release jobs must be idempotent so one reservation is not freed twice.</li></ul>
<p>My rule:</p>
<blockquote><p>If sold out beats a payment reversal for your customers, manage the last unit by reservation state—not by payment outcome alone.</p></blockquote>
<p>The last unit is not “whoever’s HTTP request arrived first.” It belongs to the order that <strong>claims stock once</strong> and keeps that promise until payment outcome is clear.</p>
<h2 id="sources">Sources<a class="heading-anchor" href="#sources" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p><a href="https://docs.stripe.com/payments/checkout/managing-limited-inventory?locale=en-GB">1</a> <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/saga-patterns.html">2</a> <a href="https://www.postgresql.org/docs/current/sql-update.html">3</a> <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Idempotency-Key">4</a> <a href="https://docs.stripe.com/api/idempotent_requests">5</a> <a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_prevent_interaction_failure_idempotent.html">6</a> <a href="https://www.postgresql.org/docs/current/transaction-iso.html">7</a> <a href="https://www.postgresql.org/docs/current/explicit-locking.html">8</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Why concurrent retries can deduct inventory twice for the same order</title>
      <link>https://changelog.encer.me/en/posts/order-idempotency-and-inventory</link>
      <guid isPermaLink="true">https://changelog.encer.me/en/posts/order-idempotency-and-inventory</guid>
      <pubDate>Mon, 26 Oct 2026 00:00:00 GMT</pubDate>
      <description>Concurrent retries of the same order attempt are not a button-disabling problem. How to separate identifying the attempt, confirming one order, deducting inventory once, and preventing duplicate downstream side effects.</description>
      <category>backend</category>
      <category>data-integrity</category>
      <category>concurrency</category>
      <category>idempotency</category>
      <category>inventory</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> When concurrent retries of the <em>same</em> order attempt both pass a naive “lookup then insert” path, inventory can be deducted twice—not because two customers raced, but because the server treated replays as new orders. Fix that with a stable idempotency key the server stores and enforces (via a DB unique constraint), explicit processing/confirmed/rejected/unknown states, and atomic order + inventory updates. Separate problem: two different customers racing for the last unit needs conditional inventory updates or locks—and event consumers still need idempotency because messages can redeliver.</p>
<p>Suppose an order API looks like this:</p>
<pre><code class="language-text">1. Look up whether the same order already exists.
2. If not, create the order.
3. Deduct inventory.</code></pre>
<p>It works fine most of the time. Then payment completes and the response drops. The user thinks the order failed and taps again. The app may retry the same request after a timeout. If two requests pass the initial lookup almost at the same time, both can decide no order exists yet. Insertion that relies on a prior read without a uniqueness constraint does not win that race.[1]</p>
<p>The outcome is not just two orders. <strong>Inventory is deducted twice.</strong></p>
<p>If you shrink the problem to “the user double-clicked,” the fix stops at disabling the button. In practice, identifying whether attempts belong to the same order, ensuring exactly one order is confirmed, deducting inventory once, and preventing downstream systems from running the same work again are separate responsibilities.</p>
<h2 id="retries-of-the-same-order-are-not-the-same-as-different-orders">Retries of the same order are not the same as different orders<a class="heading-anchor" href="#retries-of-the-same-order-are-not-the-same-as-different-orders" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Start by separating two scenarios.</p>
<ul><li><strong>Retransmission of the same order attempt:</strong> Response loss, timeouts, app restarts, or duplicate clicks can deliver one logical order request to the server multiple times. Idempotency is the API property of handling such retries without duplicate side effects.[2][5]</li><li><strong>Competition between distinct valid orders:</strong> Two customers order the last unit at the same time. Here you need to control concurrent updates to the inventory row, not reuse a request key.[4]</li></ul>
<p>Both look like inventory problems, but they are not the same failure mode. A retry of the same order must be processed once; different orders must compete fairly without overselling.</p>
<blockquote><p>An idempotency key asks: “Have we already handled this same order attempt?” Locks and conditional updates ask: “Is it safe to change this inventory right now?”</p></blockquote>
<p>One does not replace the other.</p>
<h2 id="the-same-order-attempt-needs-an-identifier">The same order attempt needs an identifier<a class="heading-anchor" href="#the-same-order-attempt-needs-an-identifier" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p><code>POST /orders</code> is a mutating API that easily creates two orders when the same request is sent twice. When the user taps order, the client should mint an <code>idempotencyKey</code> and send the same key for every retry of that attempt.</p>
<p>An API can use a stable <code>Idempotency-Key</code> header so retries of POST or PATCH do not repeat side effects—<strong>provided the server stores and enforces the key</strong>; support is API-specific and the header is non-standard.[5] The same key is only for the <strong>same retry chain</strong>; a new order attempt must use a new key.[5]</p>
<p>Without that distinction, teams often invent rules like:</p>
<blockquote><p>If the same customer orders the same product again within five minutes, treat it as a duplicate.</p></blockquote>
<p>That sounds safe but blocks legitimate orders. People buy the same product twice. They may split checkout or order again for a gift. Customer + product + a short window <strong>guesses</strong> at duplication; it does not prove intent.</p>
<p>An idempotency key names the attempt the user started. Storing the key alone is not enough: you must reject a new payload under the same key (e.g. different quantity or address). Keep a fingerprint of core fields and return an error when the same key arrives with a different request.[5]</p>
<h2 id="processing-is-neither-failure-nor-success"><code>processing</code> is neither failure nor success<a class="heading-anchor" href="#processing-is-neither-failure-nor-success" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>For requests under the same key, you need at least four outcomes:</p>
<table><thead><tr><th>State</th><th>Meaning</th><th>Response to a second request</th></tr></thead><tbody><tr><td><code>processing</code></td><td>First request still in flight</td><td>Do not create a new order; report in progress</td></tr><tr><td><code>confirmed</code></td><td>Order and inventory reservation/deduction are final</td><td>Return the existing order result</td></tr><tr><td><code>rejected</code></td><td>Out of stock or rule violation</td><td>Return the same rejection</td></tr><tr><td><code>unknown</code></td><td>External payment/communication error; outcome not finalized</td><td>Do not create a new order; offer a path to resolve status</td></tr></tbody></table>
<p>If you blur these states, the second request may fabricate success—or create another order.</p>
<p>A timeout is not immediate failure. The server may have finished the order and inventory deduction but lost the response. An external payment call may have reached the processor with an unclear outcome. Stripe advises retrying with the same key and parameters after network errors, and reconciling or confirming the original result before starting new work when the outcome is unclear.[7]</p>
<p>So <code>processing</code> and <code>unknown</code> may look awkward in the UI, but they must not be hidden. The user should see something like “Checking whether your order was accepted” and be able to poll order status. The system must not create a new order in the meantime.[2][7]</p>
<h2 id="check-then-insert-is-not-a-guarantee-under-concurrency">Check-then-insert is not a guarantee under concurrency<a class="heading-anchor" href="#check-then-insert-is-not-a-guarantee-under-concurrency" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Even with an idempotency key, this implementation is unsafe:</p>
<pre><code class="language-text">SELECT * FROM orders WHERE idempotency_key = :key

-- if no row
INSERT INTO orders (...)</code></pre>
<p>Two transactions can both see no row. “We observed no duplicate” is a momentary fact, not a promise for the next instant.</p>
<p>The rule “exactly one order” must be enforced by the database.</p>
<pre><code class="language-sql"><span class="hl-keyword">CREATE</span> <span class="hl-keyword">UNIQUE</span> <span class="hl-keyword">INDEX</span> uq_order_request
<span class="hl-keyword">ON</span> orders (customer_id, idempotency_key);</code></pre>
<p>Let the database decide conflicts:</p>
<pre><code class="language-sql"><span class="hl-keyword">INSERT</span> <span class="hl-keyword">INTO</span> orders (customer_id, idempotency_key, request_hash, status)
<span class="hl-keyword">VALUES</span> (:customer_id, :<span class="hl-keyword">key</span>, :request_hash, <span class="hl-string">'processing'</span>)
<span class="hl-keyword">ON</span> CONFLICT (customer_id, idempotency_key) DO NOTHING;</code></pre>
<p>Only the insert that succeeds proceeds with order handling. On conflict, read the existing row and return <code>processing</code>, <code>confirmed</code>, <code>rejected</code>, or <code>unknown</code> as appropriate.</p>
<p>PostgreSQL documents <code>ON CONFLICT</code> for specifying behavior on uniqueness violations.[1] Here the constraint’s job is clear: <strong>prevent two confirmed orders for the same request key.</strong>[1]</p>
<p>Do not put this index on <code>customer_id + product_id</code>. That enforces a different product policy—“you cannot buy the same product twice”—not idempotency.</p>
<h2 id="inventory-deduction-is-a-separate-integrity-rule">Inventory deduction is a separate integrity rule<a class="heading-anchor" href="#inventory-deduction-is-a-separate-integrity-rule" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Now two different customers order the last unit. Their idempotency keys differ, so you must not treat one as a duplicate of the other.</p>
<p>Inventory is safer when the database judges current stock and deduction conditions in one step, rather than reading in application code and writing later.</p>
<pre><code class="language-sql"><span class="hl-keyword">UPDATE</span> inventory
<span class="hl-keyword">SET</span> available_quantity = available_quantity - :quantity
<span class="hl-keyword">WHERE</span> product_id = :product_id
  <span class="hl-keyword">AND</span> available_quantity &gt;= :quantity;</code></pre>
<p>One affected row means success; zero means insufficient stock. This conditional update re-evaluates against current state at write time.[4]</p>
<p>Pessimistic or optimistic locking are also options. Complex rules—reservations, multi-line orders, quantity math—may require locking a row and processing serially. PostgreSQL notes explicit locking when MVCC alone is not enough.[4]</p>
<p>Locks alone do not stop <strong>re-execution of the same order attempt</strong>. After the first request deducts inventory and completes, a retry can acquire the lock and deduct again.</p>
<blockquote><p>Locks address inventory contention; idempotency keys address duplicate requests.</p></blockquote>
<h2 id="how-much-to-put-in-one-transaction">How much to put in one transaction<a class="heading-anchor" href="#how-much-to-put-in-one-transaction" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>If order rows and inventory live in the same database, start by wrapping both in one transaction.</p>
<pre><code class="language-text">1. Acquire exactly one processing order row via the idempotency key.
2. Run conditional inventory deduction.
3. On success, move the order to confirmed.
4. On insufficient stock, move the order to rejected.
5. Commit.</code></pre>
<p>That reduces intermediate states like “order succeeded but inventory failed” or “inventory dropped with no order.”</p>
<p>When the payment processor is external, you cannot finish everything in one DB transaction. Whether to reserve at authorization, deduct after payment success, or when to release reservations is product policy—not a database feature. Without that policy, even the “right” locking model produces a shaky checkout experience.</p>
<h2 id="a-safe-order-api-still-leaves-downstream-steps">A safe order API still leaves downstream steps<a class="heading-anchor" href="#a-safe-order-api-still-leaves-downstream-steps" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>After persisting the order, you may emit events to inventory, payment, shipping, and notifications. Duplication risk moves to those systems.</p>
<p>The DB commit can succeed and the process dies before publishing. Or an event goes out and the DB rolls back. Transactional outbox puts order changes and event records in the same DB transaction to reduce dual-write problems.[6]</p>
<p>Outbox does not make every downstream handler run exactly once. Messages can be delivered more than once.[3][6] Consumers for reservation, payment, and shipping must persist processed event IDs and reuse or skip on redelivery.[3]</p>
<p>Order API idempotency and consumer idempotency are contracts at different layers.</p>
<h2 id="can-t-we-just-use-a-redis-lock">“Can’t we just use a Redis lock?”<a class="heading-anchor" href="#can-t-we-just-use-a-redis-lock" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>A distributed lock (e.g. Redis) can serialize work per product. As the <strong>sole</strong> source of truth, you inherit lock expiry, network partitions, lost ownership, and failures after taking the lock but before committing to the DB.</p>
<p>Distributed locks can reduce contention in a critical section. Confirming one order is still most directly enforced with DB uniqueness; inventory floors are still most directly enforced with conditional updates or DB locks. Locks should complement those rules, not replace them.</p>
<h2 id="what-must-happen-exactly-once">What must happen exactly once?<a class="heading-anchor" href="#what-must-happen-exactly-once" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Preventing duplicate orders is not picking one technique. You need answers to:</p>
<ol><li>Does the same order attempt carry a stable identifier across retries?</li><li>Is that identifier protected by a DB uniqueness constraint?</li><li>For the same key in processing, success, rejection, or unknown, do you return distinct, correct responses?</li><li>When different orders compete, do conditional updates or appropriate locks prevent negative inventory?</li><li>Are order confirmation and inventory deduction bundled atomically where they share a database?</li><li>If payment, shipping, or notification events redeliver, do consumers avoid repeating side effects?</li><li>Does every new order get a new key?</li></ol>
<p>Code that stops duplicate orders is not code that disables a button. <strong>It is code that makes the system prove—separately—same order vs new order, stock remaining, and whether downstream work already ran.</strong></p>
<h2 id="references">References<a class="heading-anchor" href="#references" aria-label="이 섹션으로 가는 링크">#</a></h2>
<ul><li>[1] <a href="https://www.postgresql.org/docs/current/sql-insert.html">PostgreSQL — INSERT / ON CONFLICT</a></li><li>[2] <a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_prevent_interaction_failure_idempotent.html">AWS Well-Architected — Design idempotent operations</a></li><li>[3] <a href="https://microservices.io/patterns/communication-style/idempotent-consumer.html">Microservices.io — Idempotent Consumer</a></li><li>[4] <a href="https://www.postgresql.org/docs/current/explicit-locking.html">PostgreSQL — Explicit Locking</a></li><li>[5] <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Idempotency-Key">MDN — Idempotency-Key</a></li><li>[6] <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html">AWS Prescriptive Guidance — Transactional Outbox</a></li><li>[7] <a href="https://docs.stripe.com/error-low-level">Stripe — Advanced error handling</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>When AI writes code and AI reviews it, what are we still accountable for?</title>
      <link>https://changelog.encer.me/en/posts/what-are-we-responsible-for-when-ai-writes-and-reviews-code</link>
      <guid isPermaLink="true">https://changelog.encer.me/en/posts/what-are-we-responsible-for-when-ai-writes-and-reviews-code</guid>
      <pubDate>Sat, 17 Oct 2026 00:00:00 GMT</pubDate>
      <description>In an era of AI-authored and AI-reviewed code, review must leave behind accountability for intent, impact, and recovery—not just comments.</description>
      <category>ai</category>
      <category>code-review</category>
      <category>software-quality</category>
      <category>engineering-process</category>
      <content:encoded><![CDATA[<p><strong>TL;DR:</strong> AI can draft and critique code faster than humans can read every line, but merge decisions still need a human who can explain intent, behavior change, blast radius, and rollback. AI review output is comments, not ownership—teams that replace responsibility structures with “the bot approved it” lose the shared accountability peer review used to create.</p>
<p>AI writes code. Another AI reviews that code.</p>
<p>That is familiar now. Large PRs land; an AI reviewer leaves dozens of comments. The author may not have typed every line; the human reviewer may not read the full diff. AI produces plausible summaries and fix suggestions.</p>
<p>Then someone asks:</p>
<blockquote><p>Why did you choose this structure?</p>
<p>What existing behavior changes because of this?</p>
<p>If this fails in production, which data and users are affected?</p>
<p>Why does passing this test mean we are safe?</p></blockquote>
<p>If nobody can answer well, you did not do code review. You performed a ritual of machines echoing each other.</p>
<p>When things break, the dangerous sentence is not “AI wrote this code.” It is “I did not write this code.”</p>
<h2 id="review-bottlenecks-are-not-only-humans-are-slow">Review bottlenecks are not only “humans are slow”<a class="heading-anchor" href="#review-bottlenecks-are-not-only-humans-are-slow" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>The objection is obvious: line-by-line human reading will not scale with AI-generated volume.</p>
<p>Research on traditional review also does not claim humans catch every serious bug. Much feedback targets evolvability—maintainability, readability, structure, docs—more than catastrophic functional defects.[8] As change size grows, comment density falls; review effectiveness ties closely to review size.[7]</p>
<p>AI clearly helps here: repetitive violations, type errors, risky dependencies, missing-test candidates, blast-radius hints, and rule violations faster than humans alone. Studies on AI code review show promise for functional issues and noise reduction when combining multiple independent reviews.[4]</p>
<p>This is neither an argument for banning AI review nor a demand that humans read and approve every diff.</p>
<p>The trap is believing that faster generation automatically expands understanding, verification, and accountability.</p>
<h2 id="ai-review-leaves-comments-code-review-must-leave-accountability">AI review leaves comments; code review must leave accountability<a class="heading-anchor" href="#ai-review-leaves-comments-code-review-must-leave-accountability" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Evidence that AI-only review has fully replaced human review is thin.</p>
<p>A 2026 study of code-review agents analyzed 3,109 PRs from the AIDev dataset. PRs with AI reviewers only merged at 45.20%; human-only review merged at 68.37%. AI-review-only PRs also showed higher discard rates.[1]</p>
<p>Those numbers alone do not prove AI causes failure—harder changes might get AI-only review. They do not support “AI reviewed it, so drop human review.”</p>
<p>Signal quality matters more. Among discarded AI-review-only PRs, 60.2% had meaningful feedback below 30%; 12 of 13 AI reviewers studied averaged signal ratios under 60%.[1]</p>
<p>Many AI comments ≠ safer shipping.</p>
<p>LLM review struggles are not only “the model cannot read code.” Real PR review needs context outside the diff: repo structure, past design decisions, operational constraints tests do not encode, team agreements, customer-facing behavior.</p>
<p>SWR-Bench, using 1,000 real PRs with full-project context, concluded current AI review tools do not reach adequate performance in realistic settings. Top setups suffered low precision—many false positives—and verification cost returns to humans.[4]</p>
<p>AI can say “this value may be null.” It struggles with “null is acceptable for backward compatibility with last year’s app” or “this change’s cost under next week’s promo traffic” from a snippet alone.</p>
<h2 id="worse-than-what-ai-misses-is-what-humans-stop-looking-for">Worse than what AI misses is what humans stop looking for<a class="heading-anchor" href="#worse-than-what-ai-misses-is-what-humans-stop-looking-for" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Showing AI review first can feel like it helps humans review better. It can also narrow attention.</p>
<p>In a controlled study of 29 professional developers, reviewers who saw automated review first focused on locations the AI flagged. Automation found more low-severity issues but not more high-severity ones. Review time did not shrink.[14]</p>
<p>That does not make AI review useless. It means when AI sets the starting frame, human review can shrink to that frame.</p>
<p>That shift is quiet:</p>
<ul><li>Authors may not understand AI-generated code deeply.</li><li>Reviewers spend time clearing AI comments.</li><li>Design, data, operations, and customer impact AI never mentions fall off the checklist.</li><li>Nobody owns the full meaning of the change.</li></ul>
<p>So the key question in the AI era is not “how many bugs did AI catch?”</p>
<blockquote><p>Is there a person who knows why we are making this change, what changes, and who recovers if it fails?</p></blockquote>
<h2 id="peer-review-built-shared-ownership-not-just-bug-detection">Peer review built shared ownership, not just bug detection<a class="heading-anchor" href="#peer-review-built-shared-ownership-not-just-bug-detection" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>Code review is technical and social.</p>
<p>Work on accountability in code quality describes traditional peer review as expanding individual responsibility into collective responsibility. Authors explain decisions to peers; reviewers question and participate. Personal standards, professional diligence, pride in good code, and reputation within the team underpin that accountability.[2]</p>
<p>That work warns LLM-based review can weaken the mutual accountability structure. You owe an explanation to a colleague later; you are not required to explain to an LLM. An LLM is not on call when production breaks and does not decide recovery for live customers.[2]</p>
<p>“AI reviewed it” is not a quality guarantee. With no accountable owner, it can blur responsibility.</p>
<p>That is not because AI is bad—it is because teams added AI without redesigning who owns what.</p>
<h2 id="professionals-protect-the-company-they-have-now-not-only-the-model-they-hope-for-later">Professionals protect the company they have now, not only the model they hope for later<a class="heading-anchor" href="#professionals-protect-the-company-they-have-now-not-only-the-model-they-hope-for-later" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>AI will likely improve. The band where humans need not read every implementation line will grow.</p>
<p>Some argue for the end of human code review: agents can pursue defect detection, style, knowledge transfer, and team awareness cheaper and faster; humans re-reading all AI code does not scale.[10]</p>
<p>Take that seriously. Human review is imperfect; AI output volume exceeds old habits.</p>
<p>But sacrificing today’s quality and accountability for a hypothetical fully automated future is different.</p>
<p>METR’s 2025 randomized controlled trial had 16 experienced open-source developers complete 246 real tasks. Work allowed to use AI took 19% longer on average. Participants believed AI made them faster; measurement did not.[5]</p>
<p>That does not mean AI always slows developers—METR does not generalize to every task or team.[5] Other work finds faster initial implementation with AI and no clear penalty when others extend that code.[6]</p>
<p>So the lesson is not “never trust AI.”</p>
<blockquote><p>Teams that do not separate what AI is already reliable for from what humans must own are taking the wrong risk.</p></blockquote>
<p>If you are paid to ship, you cannot hollow out today’s quality process on faith in tomorrow’s cheap perfect model. Professional duty is not predicting model curves—it is making current systems explainable, recoverable, and improvable when they fail.</p>
<h2 id="reviewers-in-the-ai-era-should-not-read-every-line">Reviewers in the AI era should not read every line<a class="heading-anchor" href="#reviewers-in-the-ai-era-should-not-read-every-line" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>The answer is not more diff for humans.</p>
<p>Mechanically verifiable work belongs in code and automation:</p>
<ul><li>lint, format, typecheck</li><li>build and tests</li><li>vulnerable dependencies, secrets, license checks</li><li>static analysis</li><li>changed files and dependency impact exploration</li><li>candidate rule violations</li><li>functional decomposition and summary of large diffs</li></ul>
<p>AI can help surface issues humans might otherwise miss.</p>
<p>These still need a human name on the decision:</p>
<ul><li>why this change is needed now</li><li>what existing behavior changes</li><li>impact on users, data, operations, cost</li><li>residual risk even when tests pass</li><li>how you detect failure and roll back</li><li>who understood the risk and approved</li></ul>
<p>So AI-era PRs need a <strong>change accountability brief</strong> before—or at least alongside—the code diff:</p>
<ol><li><strong>Why</strong> — problem being solved</li><li><strong>What changes</strong> — add/modify/delete</li><li><strong>Blast radius</strong> — users, data, APIs, ops, cost</li><li><strong>Behavior delta</strong> — compatibility implications</li><li><strong>Evidence</strong> — tests, logs, runs, runbooks used to verify</li><li><strong>Risk and rollback</strong> — detection and reversal</li><li><strong>Approver</strong> — who understood and signed off</li></ol>
<p>This is not a cosmetic template. It restores understanding and ownership teams lose when AI increases code volume.</p>
<h2 id="closing">Closing<a class="heading-anchor" href="#closing" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>AI can write code and AI can review it; that share will grow.</p>
<p>After an incident, “the AI suggested it” does not restore service for customers, teammates, or the business.</p>
<p>Code review in the AI era cannot remain “read every line.” It must become clearer: AI explores, checks, summarizes, and proposes; humans own intent, impact, risk, recovery, and final accountability.</p>
<p>A professional is not someone who blindly distrusts AI-generated code.</p>
<p><strong>They design what to automate and what humans must own so the company keeps sustainable quality even as models improve.</strong></p>
<h2 id="sources">Sources<a class="heading-anchor" href="#sources" aria-label="이 섹션으로 가는 링크">#</a></h2>
<p>[1] https://dl.acm.org/doi/full/10.1145/3793302.3793614 [2] https://arxiv.org/html/2502.15963v1 [4] https://arxiv.org/html/2509.01494v1 [5] https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study [6] https://arxiv.org/abs/2507.00788v2 [7] https://ietresearch.onlinelibrary.wiley.com/doi/full/10.1049/iet-sen.2020.0134 [8] https://pmc.ncbi.nlm.nih.gov/articles/PMC9489594 [10] https://arxiv.org/html/2606.13175v1 [14] https://arxiv.org/html/2411.11401v1</p>]]></content:encoded>
    </item>
  </channel>
</rss>
