<?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: Aliasgar</title>
    <description>The latest articles on DEV Community by Aliasgar (@aliasgarmk).</description>
    <link>https://dev.to/aliasgarmk</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%2F3997671%2Fbd385679-a81a-4fa9-937a-df87a066a0d4.png</url>
      <title>DEV Community: Aliasgar</title>
      <link>https://dev.to/aliasgarmk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aliasgarmk"/>
    <language>en</language>
    <item>
      <title>The Difference Between Retry and Idempotency They're Not the Same</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Mon, 06 Jul 2026 01:56:57 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/the-difference-between-retry-and-idempotency-theyre-not-the-same-46p9</link>
      <guid>https://dev.to/aliasgarmk/the-difference-between-retry-and-idempotency-theyre-not-the-same-46p9</guid>
      <description>&lt;p&gt;Let me start with the confusion that prompted this post: in a recent mentoring session, a senior engineer described their payment service as "idempotent" because it had exponential backoff on retries. That's a category error and it's the kind of mistake that bites teams in production with real financial consequences.&lt;/p&gt;

&lt;p&gt;Retry and idempotency are complementary, not synonymous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Different Actors, Two Different Concerns
&lt;/h2&gt;

&lt;p&gt;Retry logic lives in the client. It's the client's decision to resend a request after a failure or timeout.&lt;/p&gt;

&lt;p&gt;Idempotency lives in the server. It's the server's guarantee that receiving the same request multiple times will have the same effect as receiving it once.&lt;/p&gt;

&lt;p&gt;These are independent properties. Here's what each combination looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries without server idempotency&lt;/strong&gt;: every retry is a new operation. Network timeout on a payment? Retry charges the customer again. This is the double-charge bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server idempotency without client retries&lt;/strong&gt;: the server is prepared for duplicates, but the client gives up after one timeout. The payment succeeded, the UI says error. Support tickets follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither&lt;/strong&gt;: timeouts result in unknown state, retries cause duplicates, correctness depends on luck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both, correctly implemented&lt;/strong&gt;: the client retries safely, the server deduplicates, the user sees a correct result. This is the design you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Good Retry Logic Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;MAX_ATTEMPTS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;paymentClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TimeoutException&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;ServiceUnavailableException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BASE_DELAY_MS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1L&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;JITTER_MS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentSubmissionFailedException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Max retries exceeded"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail: the same idempotency key on every retry attempt. If you're generating a new idempotency key on each retry, you've broken the connection between retry and idempotency. Each retry looks like a new request to the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Good Server-Side Idempotency Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PaymentResponse&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distributedLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaymentResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPresent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="nc"&gt;PaymentResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TTL_24H&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two non-obvious requirements here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distributed lock&lt;/strong&gt;: without a lock, two concurrent requests with the same key can both pass the "not found" check and both execute the payment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable storage&lt;/strong&gt;: the idempotency store must survive restarts. A cache with eviction can lose a key making a stored payment look new on the next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Race Condition Most Teams Miss
&lt;/h2&gt;

&lt;p&gt;Without a distributed lock:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request A arrives. Store check: not found.&lt;/li&gt;
&lt;li&gt;Request B arrives (duplicate, concurrent). Store check: not found.&lt;/li&gt;
&lt;li&gt;Request A processes payment. Stores result.&lt;/li&gt;
&lt;li&gt;Request B processes payment again. Overwrites result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Customer charged twice. The lock closes this window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Atomic Write Problem
&lt;/h2&gt;

&lt;p&gt;What if the server processes the payment but crashes before storing the idempotency key? The client retries. The server has no record of the first request. It processes again.&lt;/p&gt;

&lt;p&gt;The cleanest solution is the transactional outbox pattern: write the payment result and the idempotency record in the same database transaction, then publish events asynchronously. It's more complex but eliminates this failure mode entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Retry is client resilience. Idempotency is server correctness. You need both, explicitly connected via the idempotency key passed on every retry.&lt;/p&gt;

&lt;p&gt;When reviewing payment API designs, I check three things: Is the client sending the same key on retries? Is the server storing results durably with a lock? Is the store-and-execute step atomic? If any are missing, the system has a correctness gap that will manifest as duplicate charges under load.&lt;/p&gt;




&lt;p&gt;If you found this useful, I run 1:1 mentoring sessions for Java/backend engineers at topmate.io/aliasgar_kantawala&lt;/p&gt;

&lt;p&gt;My Java interview guides and system design resources are at aliasgarmk.gumroad.com&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Idempotency in Payment APIs — Not Optional, Not Later</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Thu, 02 Jul 2026 20:07:39 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/idempotency-in-payment-apis-not-optional-not-later-3b2j</link>
      <guid>https://dev.to/aliasgarmk/idempotency-in-payment-apis-not-optional-not-later-3b2j</guid>
      <description>&lt;p&gt;There's a version of this story that plays out at almost every fintech startup I've worked with: the team ships a payment API, it works in testing, it goes live. Then a user's connection drops mid-request. The mobile app retries. The backend processes it twice. Two charges hit the customer's account. And suddenly you're in an incident with your compliance team asking uncomfortable questions.&lt;/p&gt;

&lt;p&gt;Idempotency is the only thing standing between you and that story. After 11 years building payment systems — including integrations with instant payment rails, card processors, and core banking platforms — I have a strong opinion: if your payment API doesn't implement idempotency from day one, you've already shipped a bug. You just haven't seen it detonate yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Idempotency Actually Means
&lt;/h2&gt;

&lt;p&gt;An idempotent operation produces the same result no matter how many times you apply it. In the context of a REST API: if the client sends the same request multiple times, the server executes it exactly once and returns the same response every time.&lt;/p&gt;

&lt;p&gt;HTTP GET and DELETE are idempotent by definition. POST is not — unless you design it to be. Payment submission is POST. So you have to build idempotency yourself.&lt;/p&gt;

&lt;p&gt;The standard mechanism: the client generates a unique idempotency key (a UUID) and includes it in the request header. The server stores the key alongside the resulting response. On subsequent requests with the same key, the server returns the stored response without reprocessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Networks Make This Non-Negotiable
&lt;/h2&gt;

&lt;p&gt;Mobile clients, third-party integrators, internal services — they all operate over networks that can fail. A timeout doesn't tell the client whether the request was received. The connection might have dropped before the request arrived. Or after processing. Or mid-response.&lt;/p&gt;

&lt;p&gt;The only safe behavior for a client facing a timeout on a payment request is to retry. Which means your server must be able to receive the same payment request twice and process it exactly once. Without idempotency, you're putting the burden of deduplication on the client — which is both incorrect and unenforceable.&lt;/p&gt;

&lt;p&gt;At scale, this gets worse. Under load, timeouts become more frequent. Retries increase. Without idempotency, your duplication rate scales with your traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the Key Scope Right
&lt;/h2&gt;

&lt;p&gt;Here's where teams commonly go wrong: scoping the idempotency key to the request body hash.&lt;/p&gt;

&lt;p&gt;My recommendation: require the client to provide the idempotency key explicitly — a UUID they generate before the request. This makes deduplication intent explicit and removes ambiguity. Stripe does this. Braintree does this. There's a reason.&lt;/p&gt;

&lt;p&gt;Pair the key with the user ID and operation type in your storage key. A key generated by user A should never accidentally deduplicate a request from user B with the same UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency Is Not Caching
&lt;/h2&gt;

&lt;p&gt;This distinction matters more than it sounds. Caching is about performance — a cache miss is acceptable. Idempotency is about correctness — a "miss" on your idempotency store could result in a duplicate charge.&lt;/p&gt;

&lt;p&gt;Your idempotency store needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durability&lt;/strong&gt;: if your service restarts, you cannot lose stored idempotency records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt;: storing the key and executing the payment must be atomic, or a concurrent duplicate can slip through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL policy&lt;/strong&gt;: 24 hours is a common baseline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Redis cache with default eviction policy fails at least two of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation Skeleton (Spring Boot)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/payments"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaymentResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaymentResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPresent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Distributed lock on the key closes the race window&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distributedLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Re-check after acquiring lock&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPresent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="nc"&gt;PaymentResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paymentService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofHours&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lock on the idempotency key is critical — it closes the race window between the initial check and the store. Without it, two concurrent requests with the same key can both pass the check and both execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Idempotency is a correctness requirement for any API that mutates financial state. The clients calling your API will retry. Networks will drop packets. Your job is to make that safe.&lt;/p&gt;

&lt;p&gt;Design it in from the start. It's an order of magnitude easier than retrofitting it after your first duplicate-charge incident.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Eventual Consistency Is Misunderstood by Most Engineers</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:08:06 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/eventual-consistency-is-misunderstood-by-most-engineers-fc1</link>
      <guid>https://dev.to/aliasgarmk/eventual-consistency-is-misunderstood-by-most-engineers-fc1</guid>
      <description>&lt;p&gt;I've interviewed dozens of engineers at the senior level. When I ask them to explain eventual consistency, most recite the textbook answer: "data will converge to a consistent state — eventually." What they can't tell me is what that looks like when something goes wrong at 2 AM with real money on the line.&lt;/p&gt;

&lt;p&gt;After 11 years building distributed systems I've come to believe that eventual consistency is the most confidently misunderstood concept in backend engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Textbook Gets Right (and What It Skips)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CAP theorem tells you a distributed system can guarantee at most two of three properties: Consistency, Availability, and Partition tolerance. Under network partitions — not exceptional events, they're normal — you choose between C and A.&lt;/p&gt;

&lt;p&gt;Eventual consistency is the CP-to-AP trade. You relax the guarantee that every read reflects the most recent write, in exchange for continued availability. The system promises that, absent further writes, all replicas will converge.&lt;/p&gt;

&lt;p&gt;What the textbook skips: convergence requires you to design it. It doesn't happen by default. If two nodes accept conflicting writes, someone has to decide which one wins — and that decision has to be domain-correct, not just technically convenient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Real Reconciliation Bug&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the scenario that taught me to respect this deeply: two services consumed the same payment event stream. Service A updated the payment status. Service B, running slightly behind, read the old status — decided the payment was still pending — and updated it back.&lt;/p&gt;

&lt;p&gt;Both services behaved correctly in isolation. Both passed unit tests. The bug was emergent — a classic read-modify-write race under eventual consistency. The result was a payment that appeared confirmed in one view and pending in another. The reconciliation file we sent to Banco Central at end of day was wrong.&lt;/p&gt;

&lt;p&gt;Debugging it required correlating log timestamps across three services and figuring out that a 200ms replication lag was enough to cause the race. That's not a code bug. That's an architectural assumption that never got written down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Eventually" Has No Default SLA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When someone says "the system is eventually consistent," that "eventually" could mean 50 milliseconds or 50 minutes depending on replication topology, network conditions, and write volume.&lt;/p&gt;

&lt;p&gt;In OLTP systems under load, I've seen replication lag spike from under a second to several minutes. If your business logic assumes convergence within a heartbeat, you have an implicit SLA you've never declared — and it will fail in production at the worst possible moment.&lt;/p&gt;

&lt;p&gt;The moment I started treating eventual consistency seriously was when I started asking: what is the maximum staleness this system can tolerate? If the answer is "I don't know," that's a design gap, not a performance tuning problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict Resolution Is a Domain Problem, Not a Technical One&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most engineers reach for "last write wins" as their default conflict resolution strategy. It's simple, easy to implement, and almost always wrong in financial systems.&lt;/p&gt;

&lt;p&gt;Consider two concurrent updates to a payment record: one marks it confirmed, the other marks it as flagged for review. Last write wins gives you a coin flip on which state survives. That's not a distributed systems problem — it's a correctness problem with business consequences.&lt;/p&gt;

&lt;p&gt;The right approach is to model your domain state as a CRDT-compatible structure or to use explicit versioning and reject conflicting writes at the application layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to Choose Strong Consistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Strong consistency is expensive in latency. It requires coordination across replicas before returning a response.&lt;/p&gt;

&lt;p&gt;But there are domains where the cost of a wrong read exceeds any latency penalty you'd pay for coordination. Financial ledgers are the clearest example. If reading an account balance returns a stale value, you may approve a transaction that should be rejected. The cost — chargebacks, regulatory exposure, fraud liability — dwarfs any latency improvement.&lt;/p&gt;

&lt;p&gt;My rule: if a stale read can trigger a downstream action with irreversible consequences, use strong consistency and optimize later. You can always relax constraints. You can't always reverse a bad payment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Good Eventual Consistency Looks Like&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When eventual consistency is the right choice, the design still requires explicit work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Idempotent writes&lt;/em&gt;&lt;/strong&gt;: every write operation must be safe to retry without side effects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Version vectors or timestamps&lt;/em&gt;&lt;/strong&gt;: to detect conflicts when they occur&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Compensating transactions&lt;/em&gt;&lt;/strong&gt;: when eventual convergence reveals a conflict, you need a path to correct it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Monitoring on replication lag&lt;/em&gt;&lt;/strong&gt;: treat lag as a business metric, not just an infrastructure metric&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is accidental. It's deliberate design that most systems skip because engineers assume convergence is automatic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eventual consistency isn't a shortcut to availability — it's a contract you make with your users about the accuracy of their data. In payments and financial systems, that contract has teeth. Before you choose it, know your conflict resolution model, know your staleness tolerance, and know what happens when convergence fails.&lt;/p&gt;

&lt;p&gt;The engineers who internalize this ship more reliable systems. The ones who don't end up explaining reconciliation mismatches to compliance teams on Friday afternoons.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>softwareengineering</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>How to Design an Event-Driven Analytics Pipeline — A System Design Deep Dive</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Thu, 25 Jun 2026 22:33:19 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/how-to-design-an-event-driven-analytics-pipeline-a-system-design-deep-dive-3am1</link>
      <guid>https://dev.to/aliasgarmk/how-to-design-an-event-driven-analytics-pipeline-a-system-design-deep-dive-3am1</guid>
      <description>&lt;p&gt;&lt;em&gt;Interview prep series: real problems, real trade-offs, no hand-waving.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Worth Solving
&lt;/h2&gt;

&lt;p&gt;Here's a scenario straight from a fintech startup I worked at.&lt;/p&gt;

&lt;p&gt;We had field agents responsible for onboarding customers, managing investments, and handling deposits and withdrawals. Management wanted to shift from a fixed salary model to fixed + commission — where commission depended on each agent's monthly performance.&lt;/p&gt;

&lt;p&gt;The immediate instinct was: just write a SQL query. And that's exactly what we did, initially. But it quickly fell apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The query joined across 7+ tables representing different workflow states&lt;/li&gt;
&lt;li&gt;It had to be re-run every time someone asked "how is agent X doing?"&lt;/li&gt;
&lt;li&gt;Any change to the pay structure meant rewriting the query&lt;/li&gt;
&lt;li&gt;It didn't scale — every CXO-level ad hoc report became a fire drill&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying issue wasn't the SQL. It was that &lt;strong&gt;the system was storing events but not state&lt;/strong&gt; — and retrieving current state required replaying all those events manually every single time.&lt;/p&gt;

&lt;p&gt;This is the problem the design solves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defining the Requirements
&lt;/h2&gt;

&lt;p&gt;Before jumping to architecture, get the requirements sharp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track every meaningful user action across all front-ends (web app, customer app, agent app)&lt;/li&gt;
&lt;li&gt;Generate scheduled reports (daily customer activity, monthly agent performance)&lt;/li&gt;
&lt;li&gt;Support on-demand report generation via API&lt;/li&gt;
&lt;li&gt;Archive old data cost-effectively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Non-Functional:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The analytics pipeline must not slow down the core application&lt;/li&gt;
&lt;li&gt;Reports can tolerate some delay — this is not a real-time dashboard&lt;/li&gt;
&lt;li&gt;The system should be pluggable — drop it into any service without re-architecting that service&lt;/li&gt;
&lt;li&gt;Data older than 12 months should move to cheaper storage automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; in the requirements: low latency reads, strong consistency, sub-second aggregations. This is an &lt;strong&gt;eventually consistent, batch-oriented&lt;/strong&gt; analytics system. That shapes every decision downstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  Estimation
&lt;/h2&gt;

&lt;p&gt;Quick back-of-envelope to understand the data shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assume ~50 agent actions per agent per day, ~200 agents → ~10,000 events/day&lt;/li&gt;
&lt;li&gt;Each event ~2KB (enriched JSON with context) → ~20MB/day at raw staging&lt;/li&gt;
&lt;li&gt;Monthly reports cover ~300,000 events → manageable for Lambda batch processing&lt;/li&gt;
&lt;li&gt;After 12 months: ~7GB of raw event data → S3 archival is cost-effective at this scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this scale, you don't need a distributed streaming platform. Kafka is present here, but for a different reason — we'll get to that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: Three-Part System
&lt;/h2&gt;

&lt;p&gt;The design splits into three loosely coupled parts. This is the plug-and-play property: each part can fail or be replaced without breaking the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1 — Event Capture and Staging
&lt;/h3&gt;

&lt;p&gt;Every front-end (web, mobile, agent) fires events on meaningful user actions. The back-end receives these events, &lt;strong&gt;enriches them based on event type&lt;/strong&gt; (adds agent ID, customer context, timestamp, session metadata), and writes them to the existing RDBMS.&lt;/p&gt;

&lt;p&gt;Why RDBMS at this stage? Because each event is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Written once&lt;/strong&gt; — no updates, no reads until the cron picks it up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured&lt;/strong&gt; — enrichment at write time means the schema is predictable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactional&lt;/strong&gt; — you want writes to succeed or fail atomically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cron job runs at a configured interval, reads the unprocessed events, copies them to &lt;strong&gt;AWS S3&lt;/strong&gt;, and on success marks them for deletion. A second pass deletes the marked records. This two-step (mark → delete) pattern is intentional — if the S3 write fails mid-batch, nothing gets deleted. You get at-least-once delivery to S3, which is fine for analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data lifecycle:&lt;/strong&gt; After 12 months in S3, events are moved to local NAS (or cold storage like S3 Glacier). This keeps active storage costs low without destroying the audit trail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Web App / Mobile / Agent App]
        |
        ▼
[Back-end Service] → enriches event → [RDBMS Staging Table]
                                              |
                                        [Cron Job]
                                              |
                                        [AWS S3 Bucket]
                                              |
                              (after 12 months) → [NAS / Cold Storage]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key interview insight:&lt;/strong&gt; The cron-based approach is deliberately simple. In a higher-volume system you'd replace it with a CDC (Change Data Capture) mechanism using Debezium or similar — but at this scale, simplicity wins.&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 2 — S3 Processing and Report Generation
&lt;/h3&gt;

&lt;p&gt;Once data lands in S3, report generation becomes a data transformation problem. Python scripts and &lt;strong&gt;AWS Lambda&lt;/strong&gt; read from S3, apply aggregations, and write results (CSV, JSON) back to a designated S3 output directory.&lt;/p&gt;

&lt;p&gt;Lambda works well here because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reports are batch jobs — they run on a schedule, not continuously&lt;/li&gt;
&lt;li&gt;Individual report jobs are stateless and bounded in time&lt;/li&gt;
&lt;li&gt;Lambda scales to run multiple reports in parallel without managing servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generated reports are stored in S3 and a notification (email with a download link) is sent to the requester. The report is never pushed directly — only a link. This decouples generation latency from delivery.&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 3 — The Controller (On-Demand API + Scheduling)
&lt;/h3&gt;

&lt;p&gt;This is the operability layer — and it's often missing from system design answers.&lt;/p&gt;

&lt;p&gt;The controller is a small Java module that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedules&lt;/strong&gt; when to move data from staging to S3 (configurable intervals)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedules&lt;/strong&gt; when to trigger which reports (daily, weekly, monthly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposes a REST API&lt;/strong&gt; for on-demand report generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The on-demand API is &lt;strong&gt;fire-and-forget&lt;/strong&gt;. The client calls &lt;code&gt;POST /reports/generate&lt;/code&gt; with parameters, gets back a &lt;code&gt;202 Accepted&lt;/code&gt; immediately. The request is pushed to a &lt;strong&gt;Kafka topic&lt;/strong&gt;. A consumer picks it up, processes the report, writes to S3, and emails the link.&lt;/p&gt;

&lt;p&gt;Why Kafka here, not a simple task queue or direct Lambda invocation?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durability:&lt;/strong&gt; Kafka is log-based — messages are persisted and replayable if the consumer crashes mid-processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure:&lt;/strong&gt; If report demand spikes, Kafka buffers the requests; consumers process at their own pace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replayability:&lt;/strong&gt; If a report generation bug is found, you can replay the topic to regenerate affected reports
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Client] → POST /reports/generate
              |
        [REST Controller] → 202 Accepted
              |
        [Kafka Topic: report-requests]
              |
        [Report Consumer]
              |
        [S3 Output] → [Email Notification]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Trade-Off Discussion
&lt;/h2&gt;

&lt;p&gt;These are the questions an interviewer is likely to probe:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not stream events directly to S3 instead of staging in RDBMS first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You could use Kinesis Firehose or Kafka directly. But at this scale, staging in RDBMS first keeps the architecture within the team's existing operational expertise. The cron approach is easy to monitor, debug, and replay if something goes wrong. Streaming adds operational overhead that isn't justified until volume demands it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not use a dedicated analytics DB (Redshift, BigQuery) instead of S3 + Lambda?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For ad hoc SQL queries and dashboards, a warehouse would be better. But the requirement here is structured reports on a schedule, not exploratory analytics. S3 + Lambda is cheaper, has no idle cost, and fits the access pattern exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the cron fails halfway through a batch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mark-then-delete pattern handles this. Undeleted records will be picked up in the next cron run and re-written to S3 (idempotent if keyed properly). At-least-once delivery is acceptable for analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if report volume grows 100x?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace the cron with CDC + Kafka Streams for near-real-time event flow. Move report generation from Lambda to a Spark or Flink job on EMR for large aggregations. The S3-centric design makes this migration possible without changing the data model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Design Gets Right
&lt;/h2&gt;

&lt;p&gt;Three things worth remembering from this design:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decoupling at every boundary.&lt;/strong&gt; The front-ends don't know about S3. The cron doesn't know about report formats. The controller doesn't know about Lambda. Each part can be replaced independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Matching storage to access pattern.&lt;/strong&gt; RDBMS for write-once structured events. S3 for batch reads and cost-effective archival. Kafka for durable async processing. No single store is doing all three jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operability is part of the design.&lt;/strong&gt; The controller module — configurable schedules, on-demand API, email notifications — is what turns a pipeline into a product. Systems that work but can't be operated in production are incomplete designs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Interview Cheat Sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Why stage in RDBMS before S3?&lt;/td&gt;
&lt;td&gt;Write-once, structured, transactional — fits the access pattern; simpler than streaming at this scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why Kafka for on-demand reports?&lt;/td&gt;
&lt;td&gt;Durable, replayable, handles backpressure — fire-and-forget API needs async processing guarantee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why Lambda for report generation?&lt;/td&gt;
&lt;td&gt;Stateless batch jobs, pay-per-use, no idle cost — matches the workload perfectly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you handle cron failure?&lt;/td&gt;
&lt;td&gt;Mark-for-deletion before delete — unprocessed records are re-picked on next run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do you scale this?&lt;/td&gt;
&lt;td&gt;CDC + Kafka Streams replaces cron; Spark/Flink replaces Lambda at high volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's the data lifecycle?&lt;/td&gt;
&lt;td&gt;Hot (RDBMS staging) → Warm (S3, 12 months) → Cold (NAS/Glacier)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;The best system design answers don't start with "let's use Kafka and microservices." They start with the problem, reason through the constraints, and arrive at the simplest architecture that meets the requirements — with a clear articulation of what you'd change if those constraints shifted.&lt;/p&gt;

&lt;p&gt;This pipeline is deliberately simple. And that's the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? I write about distributed systems, fintech engineering, and senior-level interview prep at &lt;a href="https://aliasgarmk.gumroad.com/" rel="noopener noreferrer"&gt;aliasgarmk.gumroad.com&lt;/a&gt;. Drop a reaction if you'd like more designs in this series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>architecture</category>
      <category>interview</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Why Daily Transaction Volume is the Wrong Number to Quote in System Design Interviews</title>
      <dc:creator>Aliasgar</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:14:14 +0000</pubDate>
      <link>https://dev.to/aliasgarmk/why-daily-transaction-volume-is-the-wrong-number-to-quote-in-system-design-interviews-53jd</link>
      <guid>https://dev.to/aliasgarmk/why-daily-transaction-volume-is-the-wrong-number-to-quote-in-system-design-interviews-53jd</guid>
      <description>&lt;p&gt;The Mistake Most Candidates Make&lt;br&gt;
Picture this: You are in a system design interview. The interviewer asks you to design a ledger system or a payment settlement pipeline. Eager to show you can handle scale, you confidently state, "We'll design this to handle 100K transactions a day."&lt;/p&gt;

&lt;p&gt;The interviewer nods. But that number just told them very little about whether you can actually engineer the system.&lt;/p&gt;

&lt;p&gt;Leading with total daily volume is the most common trap candidates fall into. It sounds like a big, impressive number. But in reality, quoting a daily aggregate proves you are looking at the system through a marketing lens, not an engineering one.&lt;/p&gt;

&lt;p&gt;What Actually Matters: The Shape of the Workload&lt;br&gt;
Systems do not experience load as a perfectly smooth, distributed stream over 24 hours. They experience load in waves, spikes, and violent bursts. To design a resilient architecture, the raw daily number is useless. You need three specific dimensions:&lt;/p&gt;

&lt;p&gt;Peak TPS (Transactions Per Second): The absolute highest throughput the system must sustain during peak traffic.&lt;/p&gt;

&lt;p&gt;Workload Shape: How traffic distributes over time — business hours concentration, sudden marketing flashes, or scheduled batch windows.&lt;/p&gt;

&lt;p&gt;Burst Duration: How long that peak sustained load lasts. Is it a 5-second spike or a 45-minute sustained mountain?&lt;/p&gt;

&lt;p&gt;Let's do the math that most engineers skip in an interview.&lt;/p&gt;

&lt;p&gt;If you take 100K transactions and spread them evenly across 24 hours, you get roughly 1 TPS — one transaction per second. A modest, single-core database instance running on a laptop can handle that in its sleep. You don't need a distributed system for 1 TPS; you barely need a framework.&lt;/p&gt;

&lt;p&gt;But real-world systems don't work that way.&lt;/p&gt;

&lt;p&gt;A Real-World Case Study: The Deceptive Average&lt;br&gt;
I ran into this exact problem while building an end-of-day (EOD) reconciliation service for a major instant payment rail — one that processes tens of thousands of financial transactions daily and must verify every single one against an external banking ledger within a strict compliance window.&lt;/p&gt;

&lt;p&gt;On paper, the system processed around 100K transactions a day. Reasonable. Manageable. Nothing alarming.&lt;/p&gt;

&lt;p&gt;But the shape of the workload told a completely different story.&lt;/p&gt;

&lt;p&gt;Daytime traffic trickled in slowly. Then, as merchants settled their books, payroll batches triggered, and consumers made last-minute transfers, load climbed sharply toward end of day. And at the reconciliation window itself — where the external bank flushed the final ledger data in a concentrated burst — everything had to be processed before the next business day opened.&lt;/p&gt;

&lt;p&gt;When we actually measured the workload shape during that final window, the naive average and the engineering reality had almost nothing in common:&lt;/p&gt;

&lt;p&gt;Naive average: ~1 TPS, spread across 24 hours&lt;/p&gt;

&lt;p&gt;Actual peak: 22 TPS, concentrated in a 45-minute EOD window&lt;/p&gt;

&lt;p&gt;That is a 22x gap between the number you'd quote in a planning meeting and the number your system actually has to survive.&lt;/p&gt;

&lt;p&gt;Why This Changes Every Engineering Decision&lt;br&gt;
If you design for 1 TPS average, you might reasonably ask: do we even need a message queue here? A simple cron job running a sequential loop would work.&lt;/p&gt;

&lt;p&gt;But when you design for a 22 TPS burst with heavy downstream dependencies, the entire architectural conversation shifts.&lt;/p&gt;

&lt;p&gt;Thread pool sizing — your reconciliation worker can no longer be a single-threaded job. You have to reason about concurrency, resource contention, and memory footprints before writing a single line of code.&lt;/p&gt;

&lt;p&gt;Idempotency is non-negotiable — at burst load, network hiccups happen and retries are guaranteed. If your database upsert logic isn't strictly idempotent, you will be reconciling duplicates at 3 AM wondering why the ledgers don't balance.&lt;/p&gt;

&lt;p&gt;Downstream capacity limits — the external banking APIs you are calling have their own strict rate limits. Your 22 TPS ambition is irrelevant if the counterparty throttles you at 5 TPS. This forces you to design token-bucket rate limiters and robust client-side queuing — and to invest in building an in-house downstream simulator that covers all failure paths, not just the happy path.&lt;/p&gt;

&lt;p&gt;Pragmatic architecture over greenfield hype — when faced with a compressed burst window and complex recovery paths, a trendy event-driven microservices approach might introduce massive operational complexity. If the external bank operates on a secure, file-based exchange model, a robust file-chunking batch architecture might actually be the more defensible, resilient choice for Phase 1. It's boring, but it's correct.&lt;/p&gt;

&lt;p&gt;The Questions an Interviewer Actually Wants You to Ask&lt;br&gt;
Whenever an interviewer hands you a vague requirement like "Design a system that handles X hundred thousand requests a day," don't immediately start drawing boxes on the whiteboard.&lt;/p&gt;

&lt;p&gt;Stop. And ask the single question that separates senior practitioners from theorists:&lt;/p&gt;

&lt;p&gt;"What does the peak traffic look like, and over what specific window does it occur?"&lt;/p&gt;

&lt;p&gt;From there, you want to understand whether specific business events — market close, midnight clearing, payroll batches — concentrate the load into a predictable window, and what the latency and rate limits of your downstream dependencies look like precisely during that window.&lt;/p&gt;

&lt;p&gt;When you ask these questions, the interviewer knows you have actually operated systems at scale. You are no longer guessing; you are gathering the constraints required to build a real system.&lt;/p&gt;

&lt;p&gt;Closing Thought&lt;br&gt;
Daily transaction volume is a marketing number.&lt;/p&gt;

&lt;p&gt;Peak TPS over a compressed burst window is an engineering number.&lt;/p&gt;

&lt;p&gt;The next time you are in the hot seat, ignore the aggregate daily volume. Figure out the shape of the peak, calculate the true strain on the system, and design for that instead.&lt;/p&gt;

&lt;p&gt;If this kind of thinking is what you want to sharpen before your next system design round, I work with senior engineers on exactly this — translating real production experience into interview-ready depth. You can find me at topmate.io/aliasgar_kantawala.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>backendengineering</category>
      <category>interviewprep</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
