<?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: Anietimfon Effiong</title>
    <description>The latest articles on DEV Community by Anietimfon Effiong (@anietimfon_effiong_f0529f).</description>
    <link>https://dev.to/anietimfon_effiong_f0529f</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%2F1524653%2F7952c807-b0b2-4ecf-a6c3-1d31f7d2106a.jpg</url>
      <title>DEV Community: Anietimfon Effiong</title>
      <link>https://dev.to/anietimfon_effiong_f0529f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anietimfon_effiong_f0529f"/>
    <language>en</language>
    <item>
      <title>Microservices Are Not a Starting Point. They Are a Destination.</title>
      <dc:creator>Anietimfon Effiong</dc:creator>
      <pubDate>Wed, 24 Jun 2026 14:15:31 +0000</pubDate>
      <link>https://dev.to/anietimfon_effiong_f0529f/microservices-are-not-a-starting-point-they-are-a-destination-n9c</link>
      <guid>https://dev.to/anietimfon_effiong_f0529f/microservices-are-not-a-starting-point-they-are-a-destination-n9c</guid>
      <description>&lt;h1&gt;
  
  
  Microservices Are Not a Starting Point. They Are a Destination.
&lt;/h1&gt;




&lt;p&gt;I am going to say something that might get me ratio'd on Tech Twitter.&lt;/p&gt;

&lt;p&gt;Most startups do not need microservices. Not right now. Maybe not for years.&lt;/p&gt;

&lt;p&gt;I say this as someone who built one anyway.&lt;/p&gt;

&lt;p&gt;VerifiedCore — a virtual number verification platform I founded and engineered solo — runs on 14 Spring Boot 3 services. Auth, wallet, payments, notifications, delivery, analytics, eSIM, call plans, reseller, number management, and more. Kafka for async events. gRPC for synchronous calls between services. Separate MongoDB databases per service. An API gateway in front of everything.&lt;/p&gt;

&lt;p&gt;It is a genuinely complex system. And if I were starting over today, I would not build it this way. Not yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  We Learned the Wrong Lesson From Netflix
&lt;/h2&gt;

&lt;p&gt;The microservices movement was popularised by Netflix, Amazon, and Google. The argument made sense for them: massive engineering organisations, thousands of engineers stepping on each other's code, services that need to scale independently to handle hundreds of millions of users.&lt;/p&gt;

&lt;p&gt;What the industry did with that lesson was cargo-cult it.&lt;/p&gt;

&lt;p&gt;Developers read the Netflix case study and started splitting their todo-list app into a &lt;code&gt;task-service&lt;/code&gt;, a &lt;code&gt;user-service&lt;/code&gt;, and a &lt;code&gt;notification-service&lt;/code&gt; before they had a single paying customer. Teams of three engineers started debating service mesh configurations and distributed tracing tooling before they had shipped anything worth tracing.&lt;/p&gt;

&lt;p&gt;The architecture that solved Netflix's problems at 200 million users became the default starting point for teams building for 200 users. That is not how engineering decisions are supposed to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tax Is Real and Nobody Advertises It
&lt;/h2&gt;

&lt;p&gt;When you adopt microservices early, you do not just adopt the benefits. You adopt the full infrastructure bill upfront, before you have the scale to justify it.&lt;/p&gt;

&lt;p&gt;In my case, that bill looked like this:&lt;/p&gt;

&lt;p&gt;Every service needs its own Dockerfile, its own health check, its own database migration tooling, its own Kafka consumer group, its own Redis configuration, and its own Spring Security config. Miss one — as I did with the payment service — and an entire feature silently breaks in production. Not with an error you can easily trace. With a &lt;code&gt;401&lt;/code&gt; on every webhook that a payment provider sends you, meaning no payment ever completes, and you only find it by manually exercising the live flow weeks later.&lt;/p&gt;

&lt;p&gt;With 14 services, there are 14 surfaces where that kind of gap can hide.&lt;/p&gt;

&lt;p&gt;Local development became its own full-time job. My development machine could not start all 14 services simultaneously without Docker Desktop running out of memory. I had a documented procedure for starting services one at a time, waiting for each to report healthy before starting the next. JVM startup on a constrained host took anywhere from 8 to 15 minutes per service. A single rebuild could take long enough to make coffee, come back, and find it had failed silently mid-build.&lt;/p&gt;

&lt;p&gt;This is not a complaint. It is the real cost of the architecture. And at the stage I was at — a solo founder still discovering what the product needed to be — I was paying that cost with time I should have been spending on the product itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But What About Scaling?"
&lt;/h2&gt;

&lt;p&gt;This is the most common objection I hear when I make this argument, so let me address it directly.&lt;/p&gt;

&lt;p&gt;Your startup is almost certainly not at the scale where microservices provide a meaningful scaling advantage. If you are, you will know it — because you will have real traffic data telling you which specific service is the bottleneck. You will not be guessing.&lt;/p&gt;

&lt;p&gt;The argument "we might need to scale this part independently someday" is not an engineering decision. It is anxiety dressed up as architecture.&lt;/p&gt;

&lt;p&gt;A well-structured modular monolith — one where domain boundaries are clear and enforced inside the codebase — can be extracted into services later, when you have evidence that a specific module needs independent scaling. Shopify ran on a Rails monolith for years while handling billions in transactions. Stack Overflow serves millions of developers on a monolith that most engineers would call embarrassingly simple. Simple systems are debuggable. Debuggable systems ship.&lt;/p&gt;

&lt;p&gt;Extracting a service from a modular monolith is hard work. It is not insurmountable. Starting with 14 services before you understand your domain is harder — because you are locking in service boundaries before you know where the real seams are. I have refactored within a monolith. I have also renegotiated a gRPC contract across a service boundary. They are not the same category of effort.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Microservices Are Actually the Right Answer
&lt;/h2&gt;

&lt;p&gt;I want to be precise here, because this is not a binary argument.&lt;/p&gt;

&lt;p&gt;Microservices make sense when team size creates real coordination friction. When you have enough engineers that deploying one module requires synchronising with three other teams, service independence pays for itself. This is Conway's Law in action — your system architecture will mirror your communication structure. If your communication structure is two people talking across a desk, your architecture should reflect that.&lt;/p&gt;

&lt;p&gt;They make sense when you have measured a bottleneck that genuinely requires independent scaling. Not assumed one. Measured one.&lt;/p&gt;

&lt;p&gt;They make sense when different parts of the system have such different operational profiles — uptime requirements, latency tolerances, deployment cadences — that a monolith's shared fate becomes an actual liability rather than a theoretical one.&lt;/p&gt;

&lt;p&gt;None of those are circumstances that describe a startup in its first year. Maybe not in its second.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Tell Myself Before I Started
&lt;/h2&gt;

&lt;p&gt;Build one service. Call it &lt;code&gt;backend&lt;/code&gt;. Give it clean internal package structure — &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;wallet&lt;/code&gt;, &lt;code&gt;payment&lt;/code&gt;, &lt;code&gt;notification&lt;/code&gt; — with strict rules about what can call what. Write it the same way you would write the microservices version, except everything runs in one process.&lt;/p&gt;

&lt;p&gt;You will ship faster. You will find bugs faster. Your local development setup will be a single &lt;code&gt;docker compose up&lt;/code&gt;. When you eventually outgrow it, you will know exactly where to make the first cut — because your traffic data and your team structure will tell you, instead of your imagination.&lt;/p&gt;

&lt;p&gt;The goal of architecture is not to build the system that could theoretically handle your best-case future. It is to build the system that ships your product today while leaving the right doors open for tomorrow.&lt;/p&gt;

&lt;p&gt;Microservices are a destination. A modular monolith is how you earn the right to need them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build backend infrastructure and mobile products. Currently working on VerifiedCore — a virtual number verification API for developers. If this resonated, I write more pieces like this alongside technical deep-dives on Java Spring Boot and Flutter.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>beginners</category>
      <category>career</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Prevented Duplicate API Requests on Slow Mobile Networks in Flutter</title>
      <dc:creator>Anietimfon Effiong</dc:creator>
      <pubDate>Wed, 24 Jun 2026 13:54:19 +0000</pubDate>
      <link>https://dev.to/anietimfon_effiong_f0529f/how-i-prevented-duplicate-api-requests-on-slow-mobile-networks-in-flutter-27m4</link>
      <guid>https://dev.to/anietimfon_effiong_f0529f/how-i-prevented-duplicate-api-requests-on-slow-mobile-networks-in-flutter-27m4</guid>
      <description>&lt;h1&gt;
  
  
  How I Prevented Duplicate API Requests on Slow Mobile Networks in Flutter
&lt;/h1&gt;




&lt;h2&gt;
  
  
  The Bug That Only Happens in the Field
&lt;/h2&gt;

&lt;p&gt;Your app works perfectly in development.&lt;/p&gt;

&lt;p&gt;Then a farmer in rural Nigeria opens it on a 2G connection. He fills in his farm details, taps &lt;strong&gt;Save&lt;/strong&gt;, and nothing happens. So he taps again. And again.&lt;/p&gt;

&lt;p&gt;Three requests reach your server. Three farm records get created. He has no idea why.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;weak network idempotency problem&lt;/strong&gt;. The request went through. The response never came back. The app assumed failure.&lt;/p&gt;

&lt;p&gt;A loading spinner and a disabled button won't save you here. The user closes the app, reopens it, and submits again. Or the OS kills the app mid-request. You need a smarter pattern.&lt;/p&gt;

&lt;p&gt;This is how I solved it while building &lt;strong&gt;AgroXcel&lt;/strong&gt; — a Flutter platform for Nigerian farmers managing farm data and boundaries on rural 2G/3G networks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Disable the Submit Button?
&lt;/h2&gt;

&lt;p&gt;Disabling a button only protects the current UI session.&lt;/p&gt;

&lt;p&gt;It does not help when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app is killed mid-request by the OS&lt;/li&gt;
&lt;li&gt;The user reopens the app and resubmits&lt;/li&gt;
&lt;li&gt;The network drops after the request has already left the device&lt;/li&gt;
&lt;li&gt;The server processes the request but the response never returns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Idempotency solves the problem at the &lt;strong&gt;request level&lt;/strong&gt;, not the UI level. The button guard is still useful — but it is the last line of defence, not the first.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Pattern Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Taps "Save"
       ↓
Generate UUID (once — never regenerated on retry)
       ↓
Serialize payload → JSON string
       ↓
Store in Hive queue
       ↓
Send request + Idempotency-Key header
       ↓
        Success (2xx)?
       ┌──────┴──────┐
      Yes             No (network error)
       ↓                    ↓
Remove from queue    Increment retryCount in Hive
                     Retry on next reconnect
                     (max 5 attempts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server uses the key to deduplicate. Same key = same intent = process only once.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pubspec.yaml&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flutter_riverpod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.5.1&lt;/span&gt;
  &lt;span class="na"&gt;hive_flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.1.0&lt;/span&gt;
  &lt;span class="na"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^4.3.3&lt;/span&gt;
  &lt;span class="na"&gt;dio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^5.4.3&lt;/span&gt;
  &lt;span class="na"&gt;internet_connection_checker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^1.0.0+1&lt;/span&gt;  &lt;span class="c1"&gt;# connectivity_plus alone is not enough&lt;/span&gt;

&lt;span class="na"&gt;dev_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hive_generator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.0.1&lt;/span&gt;
  &lt;span class="na"&gt;build_runner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^2.4.9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Model the Pending Operation in Hive
&lt;/h2&gt;

&lt;p&gt;Store &lt;code&gt;payload&lt;/code&gt; as a &lt;strong&gt;JSON string&lt;/strong&gt;, not &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt;. Hive's generated TypeAdapters do not reliably handle nested maps at runtime — serializing to a string avoids that entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:convert'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:hive/hive.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;part&lt;/span&gt; &lt;span class="s"&gt;'pending_operation.g.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@HiveType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;typeId:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PendingOperation&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;HiveObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nd"&gt;@HiveField&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="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// the idempotency key — generated once, never changed on retry&lt;/span&gt;

  &lt;span class="nd"&gt;@HiveField&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="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// e.g. '/api/v1/farms'&lt;/span&gt;

  &lt;span class="nd"&gt;@HiveField&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="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;payloadJson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// JSON string — NOT Map&amp;lt;String, dynamic&amp;gt; (Hive adapter limitation)&lt;/span&gt;

  &lt;span class="nd"&gt;@HiveField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@HiveField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Convenience getter so callers don't have to decode manually&lt;/span&gt;
  &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;flutter pub run build_runner build&lt;/code&gt; to generate the adapter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: The Offline Queue Service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OfflineQueueService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_boxName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'pending_ops'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;_box&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Hive&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openBox&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_boxName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Call this BEFORE making any network request.&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PendingOperation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// one UUID per user intent — never recreated&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payloadJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// serialize to string for Hive compatibility&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// key by idempotency key for direct lookup&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dequeue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getPending&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: The Repository — Two Separate Methods for New vs Retry
&lt;/h2&gt;

&lt;p&gt;This is the most important design decision in the whole pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not call &lt;code&gt;saveFarm()&lt;/code&gt; when retrying.&lt;/strong&gt; That generates a new UUID and breaks the idempotency guarantee entirely. Use a dedicated &lt;code&gt;retryOperation()&lt;/code&gt; method that reuses the original key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FarmRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Dio&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;OfflineQueueService&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;FarmRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_dio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Called on first submission — generates the UUID and enqueues.&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;saveFarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;farmData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/api/v1/farms'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;farmData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sendOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Called by the retry sweep — reuses the ORIGINAL UUID. Never generates a new one.&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;retryOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sendOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sendOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PendingOperation&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;data:&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;options:&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Same key on every attempt — server deduplicates on this value.&lt;/span&gt;
          &lt;span class="s"&gt;'Idempotency-Key'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Dio throws on non-2xx by default, so reaching this line means&lt;/span&gt;
      &lt;span class="c1"&gt;// the server returned 200, 201, 202, or 204 — any successful 2xx.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dequeue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;DioException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;response&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Got a real HTTP error (4xx/5xx) — not a network issue.&lt;/span&gt;
        &lt;span class="c1"&gt;// Remove from queue: retrying a bad request won't help.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dequeue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;rethrow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Network failure — leave in Hive and increment the retry counter.&lt;/span&gt;
      &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: The Riverpod Provider
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;farmRepositoryProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FarmRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dioProvider&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offlineQueueServiceProvider&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;saveFarmProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;StateNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SaveFarmNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SaveFarmNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;farmRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SaveFarmNotifier&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StateNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AsyncValue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FarmRepository&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;SaveFarmNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;farmData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Guard against double-taps within the same session.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AsyncValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveFarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;farmData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saveFarmProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;farmData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AsyncLoading&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Save Farm'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Retry on Reconnection — With a Real Internet Check and a Cap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;connectivity_plus&lt;/code&gt; and &lt;code&gt;Connectivity()&lt;/code&gt; tell you whether the device is connected to a network — WiFi, mobile data, ethernet. They do &lt;strong&gt;not&lt;/strong&gt; tell you whether the internet is actually reachable. A device on WiFi with no uplink passes the connectivity check and still fails the request.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;internet_connection_checker&lt;/code&gt; for a real probe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConnectivityRetryService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;OfflineQueueService&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;FarmRepository&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;ConnectivityRetryService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Connectivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onConnectivityChanged&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ConnectivityResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Connectivity ≠ internet. Probe a real host before retrying.&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;hasInternet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;InternetConnectionChecker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasConnection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hasInternet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPending&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Hard cap — after 5 failures, surface an error instead of retrying forever.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryCount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// TODO: emit a notification or a Riverpod state so the UI can prompt the user.&lt;/span&gt;
          &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Use retryOperation — NOT saveFarm — to preserve the original UUID.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the Server Needs to Do
&lt;/h2&gt;

&lt;p&gt;Store the idempotency key and return the existing result on duplicate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spring Boot — same concept applies in NestJS&lt;/span&gt;
&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/farms"&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;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createFarm&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;FarmRequest&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="c1"&gt;// Return the already-created record instead of inserting a duplicate.&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;farmRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;existsByIdempotencyKey&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;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;farmRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByIdempotencyKey&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;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;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;farmService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;UNIQUE&lt;/code&gt; index on &lt;code&gt;idempotency_key&lt;/code&gt; in your migration. The database is your last line of defence if the application-level check ever races.&lt;/p&gt;

&lt;p&gt;In production, expire idempotency keys after a reasonable retention window (24–72 hours is common) to prevent unbounded table growth. A nightly cleanup job or a TTL-based index on &lt;code&gt;created_at&lt;/code&gt; handles this cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After implementing this pattern in AgroXcel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate submissions were eliminated&lt;/strong&gt; during testing on unstable 2G/3G networks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed submissions could be recovered automatically&lt;/strong&gt; after reconnection without user action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users no longer needed to re-enter data&lt;/strong&gt; after temporary network failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hive queue also gave us a side benefit: farm data is never silently lost mid-submission, even if the user gets a call, switches apps, or the OS kills the process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Generate the idempotency key &lt;strong&gt;once per user intent&lt;/strong&gt; — not per retry&lt;/li&gt;
&lt;li&gt;[ ] Store &lt;code&gt;payload&lt;/code&gt; as a JSON string in Hive — not &lt;code&gt;Map&amp;lt;String, dynamic&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Write to Hive &lt;strong&gt;before&lt;/strong&gt; the network call, never after&lt;/li&gt;
&lt;li&gt;[ ] Separate &lt;code&gt;saveFarm()&lt;/code&gt; (new key) from &lt;code&gt;retryOperation()&lt;/code&gt; (reuse key)&lt;/li&gt;
&lt;li&gt;[ ] Dequeue on any 2xx response — Dio throws on anything else by default&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;InternetConnectionChecker&lt;/code&gt; in the retry sweep, not just &lt;code&gt;Connectivity&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Implement a retry cap (5 attempts) and surface failures to the user when hit&lt;/li&gt;
&lt;li&gt;[ ] The server stores the key with a &lt;code&gt;UNIQUE&lt;/code&gt; constraint and returns the existing result on duplicate&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built while developing AgroXcel — a Flutter platform for Nigerian farmers managing farm boundaries and records on rural 2G/3G networks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>riverpod</category>
      <category>mobiledev</category>
    </item>
    <item>
      <title>Hello DEV — I'm Anietimfon, a Full-Stack Engineer from Lagos</title>
      <dc:creator>Anietimfon Effiong</dc:creator>
      <pubDate>Wed, 24 Jun 2026 13:26:38 +0000</pubDate>
      <link>https://dev.to/anietimfon_effiong_f0529f/hello-dev-im-anietimfon-a-full-stack-engineer-from-lagos-73k</link>
      <guid>https://dev.to/anietimfon_effiong_f0529f/hello-dev-im-anietimfon-a-full-stack-engineer-from-lagos-73k</guid>
      <description>&lt;p&gt;Hey DEV community 👋&lt;/p&gt;

&lt;p&gt;My name is &lt;strong&gt;Anietimfon Effiong&lt;/strong&gt;, a full-stack software engineer based in Lagos, Nigeria.&lt;/p&gt;

&lt;h2&gt;
  
  
  A slightly unusual background
&lt;/h2&gt;

&lt;p&gt;My degree is in &lt;strong&gt;Chemical Engineering&lt;/strong&gt; — not Computer Science. I graduated from the University of Uyo in 2024 with a 3.9 GPA while teaching myself software engineering on the side.&lt;/p&gt;

&lt;p&gt;I've been writing production code since 2022. The degree and the career ended up being two completely separate things, and I have zero regrets about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I build
&lt;/h2&gt;

&lt;p&gt;I work mostly in the &lt;strong&gt;backend and mobile&lt;/strong&gt; space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java Spring Boot 3&lt;/strong&gt; — microservices, gRPC, Kafka, PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flutter&lt;/strong&gt; — cross-platform mobile apps for Android and iOS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NestJS / TypeScript&lt;/strong&gt; — when the project calls for it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've shipped products in fintech, logistics, VPN infrastructure, and agricultural tech.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm working on right now
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;strong&gt;VerifiedCore&lt;/strong&gt; — a virtual number verification platform for developers. Think: rent a real phone number, receive an OTP, verify your account. Backed by a 14-service Java microservices architecture, a Flutter mobile app, and SDKs published to npm, PyPI, and Packagist.&lt;/p&gt;

&lt;p&gt;I'm also deep into AWS ML Engineering (SageMaker, MLOps) via Udacity — slowly adding machine learning to my toolkit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm here
&lt;/h2&gt;

&lt;p&gt;To share what I build and what I learn — the real stuff, including the bugs that nearly broke production.&lt;/p&gt;

&lt;p&gt;My first article is already up:&lt;br&gt;
👉 &lt;a href="https://dev.to/anietimfon_effiong_f0529f/preventing-double-spend-in-spring-boot-3-using-pessimistic-serializable-locking-12c9"&gt;Preventing Double-Spend in Spring Boot 3 Using Pessimistic SERIALIZABLE Locking&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That one came directly from a live bug I found while building VerifiedCore's wallet service. Three concurrent webhooks. One wallet. Things got interesting.&lt;/p&gt;




&lt;p&gt;If you're building in Java, Flutter, or fintech — say hi. Always happy to connect.&lt;/p&gt;

</description>
      <category>introduction</category>
      <category>java</category>
      <category>flutter</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Preventing Double-Spend in Spring Boot 3 Using Pessimistic SERIALIZABLE Locking</title>
      <dc:creator>Anietimfon Effiong</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:56:14 +0000</pubDate>
      <link>https://dev.to/anietimfon_effiong_f0529f/preventing-double-spend-in-spring-boot-3-using-pessimistic-serializable-locking-12c9</link>
      <guid>https://dev.to/anietimfon_effiong_f0529f/preventing-double-spend-in-spring-boot-3-using-pessimistic-serializable-locking-12c9</guid>
      <description>&lt;h1&gt;
  
  
  Preventing Double-Spend in Spring Boot 3 Using Pessimistic SERIALIZABLE Locking
&lt;/h1&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;You build a fintech feature. It works perfectly in testing.&lt;/p&gt;

&lt;p&gt;Then you go live.&lt;/p&gt;

&lt;p&gt;A payment provider like OPay or Stripe retries its webhook — because networks are unreliable and they want to guarantee delivery. Now you have three copies of the same webhook hitting your service at the same millisecond.&lt;/p&gt;

&lt;p&gt;Your wallet gets credited three times. Your user is now rich. Your company is not.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;double-spend&lt;/strong&gt;. And it's not a rare edge case — it's guaranteed to happen in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to check before you credit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DANGEROUS — don't do this&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;alreadyCredited&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referenceId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;walletService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&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 check and the credit are two separate operations. Between them, another thread can pass the same check. Both threads credit the wallet. You've gained nothing.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;check-then-act race condition&lt;/strong&gt;, and a standard &lt;code&gt;@Transactional&lt;/code&gt; annotation on its own will &lt;em&gt;not&lt;/em&gt; save you.&lt;/p&gt;

&lt;p&gt;By default, Spring uses &lt;code&gt;READ_COMMITTED&lt;/code&gt; isolation. Threads can still read the same "not yet credited" state from each other before either one commits. You need to go further.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Happened in VerifiedCore
&lt;/h2&gt;

&lt;p&gt;Building VerifiedCore — a virtual number verification platform on Spring Boot 3 + PostgreSQL — I hit this exact bug during live testing.&lt;/p&gt;

&lt;p&gt;I fired three concurrent webhook deliveries at the same payment reference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wallet correctly credited &lt;strong&gt;exactly once&lt;/strong&gt; — the locking worked ✅&lt;/li&gt;
&lt;li&gt;But the &lt;code&gt;payment_transactions&lt;/code&gt; row ended up showing &lt;code&gt;FAILED&lt;/code&gt; ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? A slower concurrent attempt's failure-path write committed &lt;em&gt;after&lt;/em&gt; the successful attempt's write. The last writer won, and it wrote the wrong status.&lt;/p&gt;

&lt;p&gt;Two separate races. Two different fixes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 1: Lock the Wallet Row with Pessimistic SERIALIZABLE
&lt;/h2&gt;

&lt;p&gt;The wallet-service needed to guarantee that only one credit attempt per &lt;code&gt;referenceId&lt;/code&gt; could succeed, no matter how many concurrent requests came in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Add a locking query to your repository
&lt;/h3&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="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;WalletRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Wallet&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// SELECT ... FOR UPDATE — acquires an exclusive row-level lock.&lt;/span&gt;
    &lt;span class="c1"&gt;// Every other transaction trying to read this row will BLOCK until we commit.&lt;/span&gt;
    &lt;span class="nd"&gt;@Lock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LockModeType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PESSIMISTIC_WRITE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT w FROM Wallet w WHERE w.userId = :userId"&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;Wallet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByUserIdForUpdate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;userId&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;h3&gt;
  
  
  Step 2 — Run the credit inside a SERIALIZABLE transaction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WalletService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isolation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Isolation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SERIALIZABLE&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;CreditResult&lt;/span&gt; &lt;span class="nf"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;amount&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;referenceId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// Safe to check here — PESSIMISTIC_WRITE locks the row before this read.&lt;/span&gt;
        &lt;span class="c1"&gt;// No other transaction can slip in between the check and the credit.&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;walletTransactionRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;existsByReferenceId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referenceId&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;CreditResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALREADY_CREDITED&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;Wallet&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;walletRepo&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByUserIdForUpdate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// acquires the exclusive lock&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBalanceUsd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBalanceUsd&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;walletRepo&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;wallet&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Record the referenceId so future concurrent attempts bail out above.&lt;/span&gt;
        &lt;span class="n"&gt;walletTransactionRepo&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="nc"&gt;WalletTransaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;walletId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amountUsd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;referenceId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referenceId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;CreditResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUCCESS&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;&lt;strong&gt;What this buys you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PESSIMISTIC_WRITE&lt;/code&gt; → database issues &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the wallet row&lt;/li&gt;
&lt;li&gt;Thread 1 acquires the lock. Threads 2 and 3 &lt;strong&gt;block&lt;/strong&gt; — they don't race, they wait&lt;/li&gt;
&lt;li&gt;When thread 1 commits, threads 2 and 3 wake up, see &lt;code&gt;existsByReferenceId = true&lt;/code&gt;, and return &lt;code&gt;ALREADY_CREDITED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SERIALIZABLE&lt;/code&gt; isolation also blocks &lt;strong&gt;phantom reads&lt;/strong&gt; — where a row inserted by another transaction is invisible to your current snapshot, letting a second thread pass the same existence check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live test result with three concurrent webhooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1 → CREDIT_SUCCESS    (wallet: $0.00 → $57.50)
Thread 2 → ALREADY_CREDITED  (no-op)
Thread 3 → ALREADY_CREDITED  (no-op)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly what you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix 2: Stop the Last Writer from Corrupting the Payment Row
&lt;/h2&gt;

&lt;p&gt;The wallet was safe. But the &lt;code&gt;payment_transactions&lt;/code&gt; status row wasn't.&lt;/p&gt;

&lt;p&gt;Thread 1 wrote &lt;code&gt;status = COMPLETED&lt;/code&gt;. Thread 3's failure path wrote &lt;code&gt;status = FAILED&lt;/code&gt; 200ms later — &lt;em&gt;after&lt;/em&gt; thread 1 had already committed. Last writer wins. Wrong status persists.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;optimistic locking&lt;/strong&gt; on the payment entity. One annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"payment_transactions"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentTransaction&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Hibernate increments this on every UPDATE.&lt;/span&gt;
    &lt;span class="c1"&gt;// If two threads try to update the same row version, the slower one&lt;/span&gt;
    &lt;span class="c1"&gt;// throws ObjectOptimisticLockingFailureException instead of silently&lt;/span&gt;
    &lt;span class="c1"&gt;// overwriting the winner's result.&lt;/span&gt;
    &lt;span class="nd"&gt;@Version&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ... other fields&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Hibernate now generates:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;payment_transactions&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'FAILED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;-- rejected if version already moved past 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If thread 1 committed &lt;code&gt;version 1 → 2&lt;/code&gt; (writing &lt;code&gt;COMPLETED&lt;/code&gt;), thread 3's update targets &lt;code&gt;version = 1&lt;/code&gt; — which no longer exists. PostgreSQL rejects it. No silent overwrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One important rule:&lt;/strong&gt; don't try to catch and recover inside the same &lt;code&gt;@Transactional&lt;/code&gt; method. Once Hibernate throws an optimistic lock failure, the persistence context is in an unusable state. Let it propagate to your controller:&lt;br&gt;
&lt;/p&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;"/webhooks/payment/opay"&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;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handleWebhook&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;String&lt;/span&gt; &lt;span class="n"&gt;payload&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="n"&gt;paymentService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;processConfirmedPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&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="na"&gt;build&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;ObjectOptimisticLockingFailureException&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="c1"&gt;// The row was already updated by a faster concurrent request.&lt;/span&gt;
        &lt;span class="c1"&gt;// The important guarantee (no overwrite) is already met.&lt;/span&gt;
        &lt;span class="c1"&gt;// Return 200 so the provider doesn't retry and compound the problem.&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Stale concurrent webhook for {}: ignoring"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&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="na"&gt;build&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;h2&gt;
  
  
  The Two-Layer Pattern
&lt;/h2&gt;

&lt;p&gt;These two mechanisms solve different races:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Protects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wallet balance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PESSIMISTIC_WRITE&lt;/code&gt; + &lt;code&gt;SERIALIZABLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No duplicate credit to the ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment status&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Version&lt;/code&gt; (optimistic lock)&lt;/td&gt;
&lt;td&gt;No stale FAILED overwrite on the transaction row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pessimistic locking&lt;/strong&gt; blocks threads upfront. Use it where you can't tolerate any concurrent modification — wallet balances, escrow holds, inventory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic locking&lt;/strong&gt; lets threads race and rejects the loser at commit time. Use it where conflicts are rare — status fields, audit rows, idempotency metadata.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One More Gotcha: Truncate Your Error Messages
&lt;/h2&gt;

&lt;p&gt;When the serialization error fired in VerifiedCore, &lt;code&gt;PaymentService&lt;/code&gt; tried to store the raw JDBC exception text into a &lt;code&gt;varchar(500)&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;The exception text was longer than 500 characters. The insert crashed. The transaction row got stuck in &lt;code&gt;PENDING&lt;/code&gt; forever — not even &lt;code&gt;FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Always truncate before storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;truncate&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;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Postgres throws DataException if you exceed column length.&lt;/span&gt;
    &lt;span class="c1"&gt;// Truncate here so the row always persists, even on ugly errors.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&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="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"..."&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&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;Ship the feature. Then ship the error handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Ship Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every wallet credit checks &lt;code&gt;referenceId&lt;/code&gt; &lt;strong&gt;inside&lt;/strong&gt; the locked transaction — not before acquiring the lock&lt;/li&gt;
&lt;li&gt;[ ] Your &lt;code&gt;@Transactional&lt;/code&gt; isolation is &lt;code&gt;SERIALIZABLE&lt;/code&gt; on the write path, not the Spring default &lt;code&gt;READ_COMMITTED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Your payment entity has a &lt;code&gt;@Version&lt;/code&gt; column backed by a real DB column (&lt;code&gt;bigint NOT NULL DEFAULT 0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Your webhook controller catches &lt;code&gt;ObjectOptimisticLockingFailureException&lt;/code&gt; and returns &lt;code&gt;200&lt;/code&gt; — not &lt;code&gt;500&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Your &lt;code&gt;referenceId&lt;/code&gt; column has a &lt;code&gt;UNIQUE&lt;/code&gt; index — the database is your last line of defense&lt;/li&gt;
&lt;li&gt;[ ] Your error-reason columns are long enough, or you're truncating before save&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built and battle-tested while developing &lt;a href="https://verifiedcore.com" rel="noopener noreferrer"&gt;VerifiedCore&lt;/a&gt; — a virtual number verification platform for developers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>postgres</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
