<?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: Aleksander Frolov</title>
    <description>The latest articles on DEV Community by Aleksander Frolov (@aleksander_frolov).</description>
    <link>https://dev.to/aleksander_frolov</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%2F4079810%2Fa40a6439-379c-4aee-a94a-5a9f6a7de24c.jpg</url>
      <title>DEV Community: Aleksander Frolov</title>
      <link>https://dev.to/aleksander_frolov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksander_frolov"/>
    <language>en</language>
    <item>
      <title>Idempotency and Retry in a Payment Core: Operations That Can't Be Duplicated and Can't Be Forgotten</title>
      <dc:creator>Aleksander Frolov</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:16:29 +0000</pubDate>
      <link>https://dev.to/aleksander_frolov/idempotency-and-retry-in-a-payment-core-operations-that-cant-be-duplicated-and-cant-be-forgotten-5ff7</link>
      <guid>https://dev.to/aleksander_frolov/idempotency-and-retry-in-a-payment-core-operations-that-cant-be-duplicated-and-cant-be-forgotten-5ff7</guid>
      <description>&lt;h1&gt;
  
  
  Idempotency and Retry in a Payment Core: Operations That Can't Be Duplicated and Can't Be Forgotten
&lt;/h1&gt;

&lt;p&gt;Build operations that can't be duplicated and can't be forgotten — that's the contract for a payment core.&lt;/p&gt;

&lt;p&gt;On the core I built for ROSSTRAFFY (20,000+ successful transactions/day, 99.99% uptime), the most dangerous failure class wasn't crashes. It was retries.&lt;/p&gt;

&lt;p&gt;The network doesn't guarantee delivery. A request can be processed, but the response lost — the client retries, and the server can't tell "didn't arrive" from "processed, but response lost". If a retry means "run the request again", you get double charges. If it means "resume the same operation", you get exactly one outcome per key.&lt;/p&gt;

&lt;p&gt;The contract: every payment happens exactly once — not 0, not 2 times. Zero means a lost payment and a missed discount window; two means double charges, refunds, disputes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: the payment core as a state machine
&lt;/h2&gt;

&lt;p&gt;A payment operation is not a "request" that either ran or didn't. It's a path with state: created, processing, completed, rejected, in reconciliation. Every transition must be idempotent — applying the same transition twice must not change the result.&lt;/p&gt;

&lt;p&gt;Idempotency lives at three levels, and skipping any one lets duplicates through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API level&lt;/strong&gt; — one idempotency key per business operation, generated once, surviving all retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler level&lt;/strong&gt; — the operation is a state machine; every transition checks the current state before acting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage level&lt;/strong&gt; — unique constraints as the last line of defense.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp45vdvnrxd166rexpj84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp45vdvnrxd166rexpj84.png" alt="Operation state machine: every transition is idempotent" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operation = state machine&lt;/strong&gt;: every transition idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One idempotency key per business operation&lt;/strong&gt;, generated once, surviving all retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency at three levels&lt;/strong&gt;: API, handler, storage (unique constraints as the last line of defense).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment = two operations&lt;/strong&gt;: hold (block funds) → capture (charge after the external system confirms), each with its own key and retry rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync leg&lt;/strong&gt;: bounded retries, exponential backoff, timeouts, jitter, dead-letter queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async leg (ГИС ГМП)&lt;/strong&gt;: queue + workers, load balancing across 5+ acquirers (Strategy pattern), failover on degradation → 99.8% acceptance reliability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recurring payments stay on the same acquirer&lt;/strong&gt; (confirmed token); switching only for new payments and emergencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: counters + latency per stage in Grafana, alerts on anomalies. "More retries, fewer successful operations" = retry is masking the problem, not fixing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliations as the second safety net&lt;/strong&gt; (ClickHouse: daily financial reports in 3–5 minutes instead of hours).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup22vcxn1kfiqqaf0cu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup22vcxn1kfiqqaf0cu3.png" alt="Hold and capture: exactly-once contract" width="799" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyy535uy1lyhr1wjhrcnt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyy535uy1lyhr1wjhrcnt.png" alt="Async leg: queue, balancing, payment inspector, reconciliations" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation and results
&lt;/h2&gt;

&lt;p&gt;We validated with A/B tests behind feature flags on real traffic. Effects were fractions of a percent of conversion — only real payment statistics could see them; synthetic tests couldn't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Erroneous and fraudulent operations&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−25%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment conversion&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+~10%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median transaction processing time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−15%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Idempotency isn't a nice-to-have in payments. It's the contract that makes retries safe at scale — and what turns "the user paid twice" into "the user paid once, and we can prove it".&lt;/p&gt;

&lt;p&gt;The full article with diagrams and code (EN): &lt;a href="https://frolov.guru/en/writing/idempotency-retry/" rel="noopener noreferrer"&gt;https://frolov.guru/en/writing/idempotency-retry/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;About the author: Alexander Frolov — Senior/Staff Backend Engineer (PHP, highload) and Team Lead, 18+ years: payment cores, multi-tenant platforms at federal scale. Articles on highload PHP and architecture: &lt;a href="https://frolov.guru" rel="noopener noreferrer"&gt;frolov.guru&lt;/a&gt;. If you're fighting timeouts, duplicates or lost payments — that's an idempotency problem, and it's fixable. DM or &lt;a href="mailto:aleksander@frolov.guru"&gt;aleksander@frolov.guru&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>performance</category>
      <category>php</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
