<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HaoJie Luo (Fone)</title>
    <description>The latest articles on DEV Community by HaoJie Luo (Fone) (@imminer668).</description>
    <link>https://dev.to/imminer668</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4218870%2F96707378-0aa1-44fe-a06e-dce2d6cb5e80.jpg</url>
      <title>DEV Community: HaoJie Luo (Fone)</title>
      <link>https://dev.to/imminer668</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imminer668"/>
    <language>en</language>
    <item>
      <title>Your Order API Needs More Than an Idempotency Key</title>
      <dc:creator>HaoJie Luo (Fone)</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:14:29 +0000</pubDate>
      <link>https://dev.to/imminer668/your-order-api-needs-more-than-an-idempotency-key-46j0</link>
      <guid>https://dev.to/imminer668/your-order-api-needs-more-than-an-idempotency-key-46j0</guid>
      <description>&lt;p&gt;An order API can accept an &lt;code&gt;Idempotency-Key&lt;/code&gt; header on every request and still create a second order for the same intended purchase. Generate a fresh key after a timeout, and a server that deduplicates by key has no way to connect the retry to the first attempt.&lt;/p&gt;

&lt;p&gt;There is a separate server-side problem too. A lookup followed by an insert leaves a race if competing requests can both observe that the key is absent. The key needs an atomic relationship with the saved order.&lt;/p&gt;

&lt;p&gt;That is the argument worth testing. How much does your retry guarantee actually cover?&lt;/p&gt;

&lt;p&gt;This Python and SQLite lab commits an order, deliberately drops the response, and lets you retry it. You can inspect the database instead of guessing whether the request succeeded. It creates local order records only and never charges a wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download and start the API
&lt;/h2&gt;

&lt;p&gt;Use Python 3.10 or later with &lt;code&gt;sqlite3&lt;/code&gt;, and curl. The package has no third-party dependencies. The commands below use a macOS or Linux shell; on Windows, run the curl commands in WSL. The recorded checks used Python 3.13.3 on macOS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fone.cc/wp-content/uploads/2026/10/python-sqlite-idempotent-order-api-lab-v1.zip" rel="noopener noreferrer"&gt;Download the complete checkout retry lab&lt;/a&gt;, extract it and open a terminal inside the extracted directory. You should see &lt;code&gt;checkout.py&lt;/code&gt;, &lt;code&gt;test_checkout.py&lt;/code&gt; and &lt;code&gt;README.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-B&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; unittest &lt;span class="nt"&gt;-v&lt;/span&gt;
python3 checkout.py &lt;span class="nt"&gt;--db&lt;/span&gt; checkout-demo.sqlite &lt;span class="nt"&gt;--port&lt;/span&gt; 8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite creates its own temporary database and chooses a free local port. All seven included tests passed when rerun for this draft on Python 3.13.3. The second command leaves a server running on &lt;code&gt;127.0.0.1:8765&lt;/code&gt;. Keep that terminal open and use another terminal for the requests below. If port 8765 is occupied, choose another port and change the request URLs to match.&lt;/p&gt;

&lt;p&gt;This is a local learning server. Python's documentation does not recommend &lt;code&gt;http.server&lt;/code&gt; for production. The example also has no customer authentication or merchant isolation. &lt;a href="https://docs.python.org/3/library/http.server.html" rel="noopener noreferrer"&gt;Python HTTP server documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create one order, then repeat the request
&lt;/h2&gt;

&lt;p&gt;The browser is allowed to choose a known SKU. The server's catalog owns the amount. In this example, &lt;code&gt;field-kit&lt;/code&gt; costs the synthetic raw amount &lt;code&gt;100000000&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://127.0.0.1:8765/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: reader-attempt-0001'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sku":"field-kit"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first response has status &lt;code&gt;201&lt;/code&gt;. Its JSON contains a generated order &lt;code&gt;id&lt;/code&gt;, the SKU, the saved amount and &lt;code&gt;status&lt;/code&gt; set to &lt;code&gt;open&lt;/code&gt;. Run the same command again. You should receive &lt;code&gt;200&lt;/code&gt; with the same JSON body and the same order ID.&lt;/p&gt;

&lt;p&gt;The different status codes are deliberate in this teaching API. &lt;code&gt;201&lt;/code&gt; means this request created the record; &lt;code&gt;200&lt;/code&gt; means it recovered an existing one. Other APIs have different replay contracts. Stripe, for example, documents response retention and parameter comparison in its own idempotency interface. Match the contract of the system you integrate. &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;Stripe idempotent requests&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the key and the order in the same transaction
&lt;/h2&gt;

&lt;p&gt;This is the table created by the downloaded server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request_key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unique constraint makes the key durable alongside the order. The function below, extracted from &lt;code&gt;checkout.py&lt;/code&gt;, keeps one transaction around the lookup and insert. Run it through the supplied server, which provides the imports and initializes this table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z0-9_-]{8,128}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invalid_idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CATALOG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;send_only_a_known_sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;closing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SELECT id, sku, amount FROM orders WHERE request_key=?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;idempotency_conflict&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;CATALOG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO orders VALUES (?,?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sku&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; acquires the SQLite write transaction before the lookup. A competing writer has to wait or fail, rather than independently reading an absent key and proceeding with its own insert. The unique constraint is an additional guard; the server returns success only after commit. SQLite allows one writer at a time, so this approach trades write concurrency for a small, inspectable implementation. See &lt;a href="https://www.sqlite.org/lang_transaction.html" rel="noopener noreferrer"&gt;SQLite transaction behavior&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If SQLite cannot obtain the lock within the configured timeout, the HTTP handler returns &lt;code&gt;503&lt;/code&gt; with &lt;code&gt;database_busy_retry_same_key&lt;/code&gt;. A caller should retry with backoff and keep the same purchase key. This example does not establish a production throughput limit.&lt;/p&gt;

&lt;p&gt;There is a small but consequential choice in the replay branch. It returns the amount saved with the order. It does not calculate a new price from the catalog. A price change between attempts should not silently change the purchase already accepted under that key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnsns6diesj02nhu5x7a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnsns6diesj02nhu5x7a.webp" alt="A create-order request committing one database record, losing its response and returning the same order when the purchase key is retried" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The saved purchase key connects the retry to the order that already exists.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the uncertain result
&lt;/h2&gt;

&lt;p&gt;Now use a fresh key and the lab's fault-injection header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://127.0.0.1:8765/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: reader-attempt-0002'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Demo-Drop-Response: yes'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sku":"field-kit"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Curl should report an empty response, typically exit code 52. The handler committed the order and then shut down the connection before sending response bytes. This is a controlled failure in the lab, not evidence of a real network incident.&lt;/p&gt;

&lt;p&gt;Retry without the fault header, keeping the same key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://127.0.0.1:8765/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: reader-attempt-0002'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sku":"field-kit"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response should be &lt;code&gt;200&lt;/code&gt;. The test suite checks that the disconnected call left one database row and the retry did not add another. If you repeat the fault-injected call after running the example before, the saved order will already exist; the database filename is intentionally persistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now change only the key
&lt;/h2&gt;

&lt;p&gt;After recovering &lt;code&gt;reader-attempt-0002&lt;/code&gt;, send the same purchase under a new key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://127.0.0.1:8765/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: reader-attempt-0003'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sku":"field-kit"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a fresh run, this returns &lt;code&gt;201&lt;/code&gt; and a different order ID. Both requests have an idempotency header. The database still contains two separate purchases for those two keys.&lt;/p&gt;

&lt;p&gt;Inspect the records from another terminal in the same directory. Stop the API with Ctrl+C when you are done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;CHECK&lt;/span&gt;&lt;span class="sh"&gt;'
import sqlite3
with sqlite3.connect('checkout-demo.sqlite') as db:
    for row in db.execute(
        "SELECT request_key, id, sku, amount FROM orders ORDER BY request_key"
    ):
        print(row)
&lt;/span&gt;&lt;span class="no"&gt;CHECK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you followed all the commands once with a fresh database, expect three rows in total, one for each of &lt;code&gt;0001&lt;/code&gt;, &lt;code&gt;0002&lt;/code&gt; and &lt;code&gt;0003&lt;/code&gt;. Repeating the commands against that same database reuses those rows.&lt;/p&gt;

&lt;p&gt;The client needs to keep a key for the lifetime of one purchase attempt, including uncertain outcomes. If your business needs to recognize the same purchase across tabs, devices or newly generated keys, it also needs a separate business identity and uniqueness rule. Two identical baskets can be two legitimate purchases, so deduplicating by basket contents alone would make a different mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject a changed purchase under the same key
&lt;/h2&gt;

&lt;p&gt;Reuse &lt;code&gt;reader-attempt-0001&lt;/code&gt;, but ask for the other SKU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://127.0.0.1:8765/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Idempotency-Key: reader-attempt-0001'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"sku":"field-notes"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect &lt;code&gt;409&lt;/code&gt; and &lt;code&gt;idempotency_conflict&lt;/code&gt;. The existing purchase remains unchanged. Sending an extra &lt;code&gt;amount&lt;/code&gt; field instead returns &lt;code&gt;400&lt;/code&gt;; the client cannot override the server price through this endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this guarantee ends
&lt;/h2&gt;

&lt;p&gt;The test suite sends twelve HTTP requests from six client threads and checks that they produce one order ID. It also restarts the server against the same database. Those are useful checks of this local service, not a distributed exactly-once guarantee or a power-loss test.&lt;/p&gt;

&lt;p&gt;For a payment checkout, the next boundary is the call to a payment provider or blockchain. A committed SQLite row cannot by itself prevent a second external charge. Define a stable identity for that external operation and a recovery path for an uncertain result before adding real payments.&lt;/p&gt;

&lt;p&gt;This sample also lacks authentication and merchant isolation. In a multi-tenant API, derive the key's scope from the authenticated account and operation. Define which request fields must match and how long the key remains valid. Remove the demonstration disconnect header before adapting the pattern to a deployed service.&lt;/p&gt;

&lt;p&gt;A practical review question is whether the integration can recover the original order after its response is lost. “We send an idempotency header” leaves that question unanswered.&lt;/p&gt;

&lt;p&gt;For an API you maintain, would you return the original &lt;code&gt;201&lt;/code&gt; on replay, or use &lt;code&gt;200&lt;/code&gt; as this lab does? The important part is documenting the contract so the caller can handle a recovered purchase correctly.&lt;/p&gt;

&lt;p&gt;Adapted from &lt;a href="https://fone.cc/insights/python-sqlite-idempotent-order-api/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=dev_st_l01_20261031" rel="noopener noreferrer"&gt;my original Python and SQLite tutorial&lt;/a&gt;. The runnable lab and illustrations are available there as well.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This draft was prepared by an AI coding agent from my existing tutorial and code. The seven included tests were rerun, and additional checks confirmed that a new key creates a second order and that replay keeps the saved price. This is a local teaching example, not evidence of a production payment deployment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>database</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
