<?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: Miguel Shinyenyi</title>
    <description>The latest articles on DEV Community by Miguel Shinyenyi (@miguel_shinyenyi_e2291c8c).</description>
    <link>https://dev.to/miguel_shinyenyi_e2291c8c</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%2F2292980%2F4251fa04-8b57-4cec-bc4a-88c1a8ac7a2a.jpg</url>
      <title>DEV Community: Miguel Shinyenyi</title>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/miguel_shinyenyi_e2291c8c"/>
    <language>en</language>
    <item>
      <title>You Cannot Stop What Arrives</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 01:51:52 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/you-cannot-stop-what-arrives-co8</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/you-cannot-stop-what-arrives-co8</guid>
      <description>&lt;h2&gt;
  
  
  What I notice
&lt;/h2&gt;

&lt;p&gt;I cannot stop my thoughts. Try it yourself. Set a timer for thirty seconds and think of nothing. A thought will arrive before you pick it. This is my own observation. I have not checked the research on it.&lt;/p&gt;

&lt;p&gt;Some thoughts arrive with weight. One kind is a standard I did not choose. It asks me to be someone before I have done anything. I cannot meet that kind by working, because it never says what finished looks like.&lt;/p&gt;

&lt;p&gt;This piece started from a line in the Marvel series VisionQuest, about holding someone's memories without their feelings. I have not tried to explain the show. I took the line as a place to start thinking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three answers to an arrival
&lt;/h2&gt;

&lt;p&gt;I see three ways to answer a thought that weighs too much.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Act on it at once, as if it were an order.&lt;/li&gt;
&lt;li&gt;Shrink so the test never comes. If I never try, I cannot fall short.&lt;/li&gt;
&lt;li&gt;Record what is true, decide what I can, and leave the rest marked as unknown.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second answer is the one I want to look at. It feels like relief. It also removes the one thing that would tell me whether the weight was real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape in a settlement engine
&lt;/h2&gt;

&lt;p&gt;My settlement engine cannot choose what arrives either. A payment gateway times out. Postgres deadlocks two transactions against each other. Neither event waits for permission. What I control is the state the engine writes next.&lt;/p&gt;

&lt;p&gt;Here is the first decision, trimmed from &lt;code&gt;SettlementService.createSettlement&lt;/code&gt;:&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="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gatewayResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;externalSettlementGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executionRequest&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;Exception&lt;/span&gt; &lt;span class="n"&gt;gatewayFailure&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gatewayResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GatewayResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SettlementOutcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UNKNOWN&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="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;finalizeWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executionRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;gatewayResult&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timeout means the engine does not know whether the gateway processed the payment. The code says so. It writes UNKNOWN, not CONFIRMED and not FAILED. The comment in the file gives the reason: leaving the settlement PENDING would leave its idempotency key IN_PROGRESS, and every retry with that key would get a 409 with no way to resolve it.&lt;/p&gt;

&lt;p&gt;Here is the second decision, trimmed from &lt;code&gt;finalizeWithRetry&lt;/code&gt;:&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="k"&gt;for&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;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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_FINALIZE_ATTEMPTS&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="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;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;finalizeSettlement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gatewayResult&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;TransientDataAccessException&lt;/span&gt; &lt;span class="n"&gt;transientFailure&lt;/span&gt;&lt;span class="o"&gt;)&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;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;MAX_FINALIZE_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;return&lt;/span&gt; &lt;span class="nf"&gt;finalizeAsUnknownAfterExhaustingRetries&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transientFailure&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;meterRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"settlement.finalize.retries"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;sleepBriefly&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last branch is the part that matters. A comment above it says the Phase 9 load test found a bug. When the five attempts ran out with no fallback, the settlement stayed PENDING and its key stayed IN_PROGRESS forever. The failure was not the deadlock. Deadlocks were expected and retried. The failure was that the engine reached a point where it made no decision, and a state with no decision never ends.&lt;/p&gt;

&lt;p&gt;The fix gave the engine a second budget of five attempts that writes UNKNOWN. UNKNOWN is a smaller claim than CONFIRMED. It is also true, and a later reconciliation can resolve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from the comparison
&lt;/h2&gt;

&lt;p&gt;I do not need to claim greatness, and I do not need to claim failure. Both are large claims, and the evidence for either does not exist yet. The smaller true claim is available: here is what I made today. For anything bigger I can write unknown, and let the record settle it later. My journal does the job that reconciliation does for the engine.&lt;/p&gt;

&lt;p&gt;The stall is the bug. Shrinking so the test never comes is the PENDING state with the key stuck. It looks like safety. It blocks every later attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the comparison stops
&lt;/h2&gt;

&lt;p&gt;A person is not a service. Three differences matter.&lt;/p&gt;

&lt;p&gt;The engine stops after ten attempts. A person should not cap attempts at ten.&lt;/p&gt;

&lt;p&gt;The engine's UNKNOWN resolves against an outside system that holds the answer. A record of my own work does not hold the answer to my worth. It only holds what I did.&lt;/p&gt;

&lt;p&gt;The engine decides in milliseconds. This decision takes years, and I may get it wrong in both directions. The feeling that I could do great things is a feeling, not evidence. It may be potential. It may be the weight of an expectation. I cannot tell which from inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not know
&lt;/h2&gt;

&lt;p&gt;I do not know how common the shrinking answer is. I am drawing on my own experience, not on data.&lt;/p&gt;

&lt;p&gt;I read only &lt;code&gt;createSettlement&lt;/code&gt; and &lt;code&gt;finalizeWithRetry&lt;/code&gt; for this piece. I am still restudying the rest of the retry path, so I make no claim about how it behaves under load beyond what the code comments say.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;I cannot stop what arrives. I can choose the state I write next.&lt;/li&gt;
&lt;li&gt;A state that never ends is worse than an honest "unknown".&lt;/li&gt;
&lt;li&gt;Make the next claim small enough to be true: what I made today.&lt;/li&gt;
&lt;li&gt;Keep the record. It settles questions that a feeling at 4 in the morning cannot.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/you-cannot-stop-what-arrives/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>philosophy</category>
      <category>systemsthinking</category>
      <category>java</category>
      <category>visionquest</category>
    </item>
    <item>
      <title>A Convenience Is Hard to Give Back</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:18:31 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/a-convenience-is-hard-to-give-back-1ico</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/a-convenience-is-hard-to-give-back-1ico</guid>
      <description>&lt;h2&gt;
  
  
  The claim, as I understand it
&lt;/h2&gt;

&lt;p&gt;This is my understanding of the "luxury trap" passage in Sapiens. It is not a checked account&lt;br&gt;
of the text.&lt;/p&gt;

&lt;p&gt;As I read it, farming was not given up because each change was small. A bit more grain, a bit&lt;br&gt;
more settling, spread over many generations. By the time the cost showed, nobody remembered&lt;br&gt;
another way to live. The extra food had already become the number people lived on.&lt;/p&gt;

&lt;p&gt;My own observation, not the book's: the same thing happens now. A convenience arrives, it&lt;br&gt;
becomes normal, and then it brings work of its own. Email saved time. It also made people&lt;br&gt;
answer faster, and many of us now feel behind on it all day.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three parts, as I see them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Each step is small and it helps. Nobody argues against it.&lt;/li&gt;
&lt;li&gt;The step becomes normal, and other things get built on it.&lt;/li&gt;
&lt;li&gt;Going back now costs more than staying, so it does not happen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My earlier piece on the farming chapter asked what a measure counts and what it leaves out. This one&lt;br&gt;
asks something else: why nobody steps back once the cost is visible.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same shape in code
&lt;/h2&gt;

&lt;p&gt;A payment call sometimes times out. Adding a retry is one small step. Callers stop handling&lt;br&gt;
timeouts and the code gets shorter. This is a toy, run on one machine, with the reply lost&lt;br&gt;
exactly once:&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;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;withRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Supplier&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;call&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;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;for&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&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;call&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="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="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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&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="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;Gateway&lt;/span&gt; &lt;span class="n"&gt;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gateway&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;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withRetry&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;plain&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="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&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="c1"&gt;// Retry, no key:   receipt=R2 charges=2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call moved the money and lost the reply. The retry moved it again. The customer was&lt;br&gt;
charged twice, and the convenience caused it.&lt;/p&gt;

&lt;p&gt;The fix is a key made once and sent on every attempt. The server remembers it:&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="nc"&gt;Gateway&lt;/span&gt; &lt;span class="n"&gt;keyed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gateway&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;key&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;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// made once, reused on every retry&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withRetry&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;keyed&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;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&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="c1"&gt;// Retry, with key: receipt=R1 charges=1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixes it, and it adds work. The server needs a place to store keys. Something has to&lt;br&gt;
decide how long a key lives. A second request can arrive while the first is still running, so&lt;br&gt;
a key needs a state. In the settlement engine I built, the design has those pieces: keys with&lt;br&gt;
an in-progress and a completed state, and a scheduled sweep for the case where a process&lt;br&gt;
crashed before it finished, which would leave a key stuck in progress. I am describing the design as it stands, not the order I built&lt;br&gt;
it in.&lt;/p&gt;

&lt;p&gt;Now take the retry out. Every caller has stopped handling timeouts. Every server is built to&lt;br&gt;
expect repeats. Removing one line is not a one-line change anymore. That is the third part&lt;br&gt;
above, in a place I can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the comparison stops
&lt;/h2&gt;

&lt;p&gt;The first farmers could not see where it led. I can see this one. I can read the cost in code&lt;br&gt;
before I add the step, and I can choose.&lt;/p&gt;

&lt;p&gt;That does not make the step reversible. It only lets me price it first. A habit I want to try&lt;br&gt;
before adding any convenience: write down the work I will have to keep doing because of it,&lt;br&gt;
and who will start depending on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not know
&lt;/h2&gt;

&lt;p&gt;I do not know whether AI tools save me hours, or raise what I expect of myself in the same&lt;br&gt;
hours. That is the luxury trap question, asked of my own week. I have a feeling about it, and&lt;br&gt;
a feeling is not a measure. A test I have not run: log hours and output for two weeks, with&lt;br&gt;
and without the tool, and compare.&lt;/p&gt;

&lt;p&gt;Other limits. I have not checked the chapter's evidence. The email point is my observation.&lt;br&gt;
The retry example is a toy with one lost reply. The link to the settlement engine describes&lt;br&gt;
its design, not a history of how I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A convenience that helps today can add work you cannot stop doing tomorrow. Look for that
work before you add it.&lt;/li&gt;
&lt;li&gt;Small steps hide the cost. The first one is the cheapest time to price it.&lt;/li&gt;
&lt;li&gt;Seeing the trap does not undo it. Reversing gets harder as other things depend on the step.&lt;/li&gt;
&lt;li&gt;A feeling that a tool saves time is not a measure. Count the hours.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/a-convenience-is-hard-to-give-back/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sapiens</category>
      <category>systemsthinking</category>
      <category>idempotency</category>
    </item>
    <item>
      <title>More Copies Is Not a Better Life</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:06:05 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/more-copies-is-not-a-better-life-559c</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/more-copies-is-not-a-better-life-559c</guid>
      <description>&lt;h2&gt;
  
  
  The claim, as I understand it
&lt;/h2&gt;

&lt;p&gt;This is my understanding of Part 2, Chapter 1 of Sapiens, "History's Biggest Fraud". It is&lt;br&gt;
not a checked account of the text.&lt;/p&gt;

&lt;p&gt;As I read it, the chapter says farming did not make people smarter or their lives easier.&lt;br&gt;
The average farmer worked harder than the average forager and ate a narrower diet. The&lt;br&gt;
chapter blames a few plants: wheat, rice, potatoes. They planned nothing. But the people who&lt;br&gt;
tended them left more descendants, so the habit spread, and the plants now cover far more&lt;br&gt;
ground than before. People settled next to the fields and worked to keep them alive. In the&lt;br&gt;
chapter's framing, the plants domesticated us.&lt;/p&gt;

&lt;p&gt;I can check one part of this outside the book. Researchers compare skeletons from before and&lt;br&gt;
after farming arrived. I found two sources: a 2011 paper on stature and robusticity across the&lt;br&gt;
agricultural transition, and a review titled "Foraging to Farming Transition: Global Health&lt;br&gt;
Impacts, Trends, and Variation". I could not open either. I only confirmed that they exist.&lt;br&gt;
Both titles point at health costs, and the second points at variation between regions. Treat&lt;br&gt;
that as a lead, not a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two numbers that moved apart
&lt;/h2&gt;

&lt;p&gt;The chapter's point, as I take it: evolution measures success in copies of DNA. More people&lt;br&gt;
means more copies. By that measure farming won, for the plants and for our species. The same&lt;br&gt;
measure has no column for how any one person's life went.&lt;/p&gt;

&lt;p&gt;Farming kept more people alive under worse conditions. Both halves of that sentence are true&lt;br&gt;
at once, because they count different things. One counts how many. The other counts how well.&lt;/p&gt;

&lt;p&gt;The chapter also warns against judging thousands of years by how we live now. Today's&lt;br&gt;
security and wealth sit on what farming made possible. A bad deal for the average farmer and&lt;br&gt;
a base for later life can both be true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where my company comparison holds
&lt;/h2&gt;

&lt;p&gt;I compared it to a company. A company's goal is to make money, not to keep its employees&lt;br&gt;
happy. Revenue can rise while happiness falls, because happiness is not in the number.&lt;/p&gt;

&lt;p&gt;That is the same structure: one measure, and people it does not count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops holding
&lt;/h2&gt;

&lt;p&gt;Wheat cannot notice anything. A company has people in it who can notice the gap and close it.&lt;br&gt;
Some leaders treat employee happiness as a way to keep staff and raise output. Some treat it&lt;br&gt;
as a goal. Either way, a person decided. Evolution has nobody to decide.&lt;/p&gt;

&lt;p&gt;So the version of the comparison I trust is smaller. A system judged by one number drifts away&lt;br&gt;
from the welfare of the people inside it, unless someone ties the two together on purpose.&lt;/p&gt;

&lt;p&gt;A company whose growth and employee welfare both rise today has not shown that anyone tied&lt;br&gt;
them. They may be moving together for now. A test: when the number is threatened, what gets&lt;br&gt;
cut first? If employee welfare goes first, the tie was never there. I have not applied this&lt;br&gt;
test to any company yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am not sure about
&lt;/h2&gt;

&lt;p&gt;I have not read the chapter's figures on hours worked or diet. "Worked harder, ate worse" is&lt;br&gt;
the chapter's claim to me, not one I checked. The skeleton evidence appears to vary by region,&lt;br&gt;
going by the titles alone.&lt;/p&gt;

&lt;p&gt;Farming also gave us things foragers did not have: surplus, people who could stop growing&lt;br&gt;
food, writing, cities. "Fraud" describes one axis, the average person's daily life. I do not&lt;br&gt;
know how to weigh it against the others, and I am not sure the chapter claims to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A system can win by its own measure while the people inside it lose by theirs. Ask what the
number counts and what it leaves out.&lt;/li&gt;
&lt;li&gt;More is not better. Copies, revenue and output all count quantity.&lt;/li&gt;
&lt;li&gt;A comparison is useful until it stops fitting. Say where it stops. Here: wheat cannot
choose, a company can.&lt;/li&gt;
&lt;li&gt;A measure lines up with people's welfare on purpose only if something protects the link when
protecting it costs. What gets cut first shows which one the system serves.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/more-copies-is-not-a-better-life/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sapiens</category>
      <category>incentives</category>
      <category>systemsthinking</category>
    </item>
    <item>
      <title>What the Reconciler Can't See</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:05:32 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/what-the-reconciler-cant-see-469g</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/what-the-reconciler-cant-see-469g</guid>
      <description>&lt;h2&gt;
  
  
  Explain it cold, then check
&lt;/h2&gt;

&lt;p&gt;Same method as before: explain the subsystem from memory, then check it against the code.&lt;/p&gt;

&lt;p&gt;My cold answer was this. Reconciliation is the last resort. When the system cannot settle a&lt;br&gt;
payment on its own, it marks the payment UNKNOWN and pushes it to a place where someone is&lt;br&gt;
alerted. That person looks, finds what went wrong, and either records an answer or edits the&lt;br&gt;
data.&lt;/p&gt;

&lt;p&gt;The code disagreed with that in three places. It also showed me a fourth problem I had not&lt;br&gt;
thought to ask about.&lt;/p&gt;
&lt;h2&gt;
  
  
  Unknown and conflicting are different problems
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReconciliationService.runOnce&lt;/code&gt; loads every settlement that has an external reference and&lt;br&gt;
compares each one to the external system's own record. This is what it does with one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;look up the external record
  none, under 5 min   -&amp;gt; wait
  none, over 5 min    -&amp;gt; mismatch
  amount or currency
    differs           -&amp;gt; mismatch
  settlement UNKNOWN  -&amp;gt; adopt external
  both CONFIRMED or
    both FAILED       -&amp;gt; nothing
  anything else       -&amp;gt; mismatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had filed UNKNOWN under "needs a person". It does not. If the settlement is UNKNOWN and the&lt;br&gt;
external record agrees on amount and currency, the engine settles it by itself, using the same&lt;br&gt;
method the original request would have used:&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="nc"&gt;SettlementOutcome&lt;/span&gt; &lt;span class="n"&gt;resolvedOutcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;externalRecord&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="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;ExternalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONFIRMED&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;SettlementOutcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONFIRMED&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SettlementOutcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;finalizeSettlement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlement&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GatewayResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolvedOutcome&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExternalRef&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A person is needed only when the two sides contradict each other. The project's own doc gives&lt;br&gt;
the reason: if a settlement is already CONFIRMED and the external system now disputes it, money&lt;br&gt;
may have moved, and only a person can judge what to do. So the rule is: evidence that fills a&lt;br&gt;
gap gets applied automatically. Evidence that conflicts waits for a person.&lt;/p&gt;
&lt;h2&gt;
  
  
  It runs on a timer, not on failure
&lt;/h2&gt;

&lt;p&gt;A scheduler calls &lt;code&gt;runOnce&lt;/code&gt; every 60 seconds by default, whether or not anything went wrong.&lt;br&gt;
Each pass loads every settlement that has ever had a reference, with no date window and no&lt;br&gt;
paging, and asks the external side about each one. That is fine at this size. At real volume,&lt;br&gt;
the same query is also the cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  The query decides what it can see
&lt;/h2&gt;

&lt;p&gt;The candidate query is &lt;code&gt;findByExternalRefIsNotNull()&lt;/code&gt;. A settlement only gets a reference&lt;br&gt;
inside &lt;code&gt;finalizeSettlement&lt;/code&gt;, and only when the gateway returns one:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;externalRef&lt;/span&gt;&lt;span class="o"&gt;()&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="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setExternalRef&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gatewayResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;externalRef&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;Three paths end in UNKNOWN with no reference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The gateway call throws. &lt;code&gt;SettlementService&lt;/code&gt; builds &lt;code&gt;new GatewayResult(UNKNOWN, null)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The finalize retry budget runs out. The fallback uses the same null.&lt;/li&gt;
&lt;li&gt;The stale-pending sweep finalizes an orphaned PENDING settlement as UNKNOWN, also with
null.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these can reach the auto-resolve step, because reconciliation never loads them. They&lt;br&gt;
do not open a mismatch either. They stay UNKNOWN.&lt;/p&gt;

&lt;p&gt;The case that worries me most is the second failure type from the project's design doc: the&lt;br&gt;
external system processed the payment, and the response was lost. The money moved. The engine&lt;br&gt;
never learned the reference. The project's own open-questions list names this and two fixes:&lt;br&gt;
ask the external system what it received for this settlement, or match on amount, date and&lt;br&gt;
account. Neither is built. I checked the code instead of trusting the doc, and the doc was&lt;br&gt;
right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody is told
&lt;/h2&gt;

&lt;p&gt;I said the person gets alerted. In the code, a new mismatch is published as the event&lt;br&gt;
&lt;code&gt;reconciliation.mismatch_found&lt;/code&gt;. Nothing consumes it. The Kafka doc says "No consumer yet". The&lt;br&gt;
Prometheus rules cover three things: the backend being down, the ML service being down, and a&lt;br&gt;
spike in failed settlements. None covers mismatches.&lt;/p&gt;

&lt;p&gt;So a mismatch waits until someone opens the reconciliation page. A record in a table&lt;br&gt;
guarantees a person can find the problem. It does not guarantee a person will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording a decision is not making it happen
&lt;/h2&gt;

&lt;p&gt;The resolve endpoint takes a reason, marks the mismatch resolved, and writes an audit row and&lt;br&gt;
an event. Its own description says it does not change the settlement or the ledger.&lt;/p&gt;

&lt;p&gt;For ledger mismatches the docs say what follows: if the data still disagrees, the next read of&lt;br&gt;
that account opens a new mismatch. Reading &lt;code&gt;flagMismatch&lt;/code&gt;, the same should hold for settlement&lt;br&gt;
mismatches, since it only skips a settlement that already has an open row. I read that. I have&lt;br&gt;
not run it.&lt;/p&gt;

&lt;p&gt;No endpoint corrects data. &lt;code&gt;REVERSED&lt;/code&gt; exists in the state machine and is reachable from&lt;br&gt;
CONFIRMED, but nothing in the code ever moves a settlement there. Today the correction is a&lt;br&gt;
person editing the database by hand. That edit skips the state machine, the ledger entries and&lt;br&gt;
the outbox event, because all three live in application code.&lt;/p&gt;

&lt;p&gt;My first answer was "a person edits it". Keeping the decision with a person is right. The&lt;br&gt;
open question is who carries it out. Either the person edits rows directly, or the person&lt;br&gt;
decides and the engine executes it through the same paths as every other money movement. I&lt;br&gt;
have not decided between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Evidence that fills a gap can be applied automatically. Evidence that conflicts needs a
person.&lt;/li&gt;
&lt;li&gt;A checker sees only what its query returns. Ask what the query cannot return.&lt;/li&gt;
&lt;li&gt;A record in a table is not an alert. Someone has to be told.&lt;/li&gt;
&lt;li&gt;Recording a decision does not carry it out. Decide who does.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/tech/what-the-reconciler-cant-see/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>postgres</category>
      <category>reconciliation</category>
      <category>kafka</category>
    </item>
    <item>
      <title>What Becomes True When Both Stay</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:54:22 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/what-becomes-true-when-both-stay-11k2</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/what-becomes-true-when-both-stay-11k2</guid>
      <description>&lt;h2&gt;
  
  
  The curtain of silence
&lt;/h2&gt;

&lt;p&gt;Sapiens describes the evidence for prehistoric life as a Rorschach test. A burial site with&lt;br&gt;
ornaments on a skeleton might mean hierarchy. It might mean something else entirely. A&lt;br&gt;
fracture on an ancient bone might mean violence. It might not. The book calls this its&lt;br&gt;
Curtain of Silence, evidence thin enough that even careful scholarship can't get a clean&lt;br&gt;
answer through it. Scholars keep asking anyway, because the past still shaped what came&lt;br&gt;
after it, but the honest version of that inquiry holds its answers loosely.&lt;/p&gt;

&lt;p&gt;That's a claim about archaeology. It turned out to be useful somewhere closer to home.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two truths instead of one
&lt;/h2&gt;

&lt;p&gt;Dialectical thinking, how my mind works?,&lt;br&gt;
it generates a position and its counter-argument at the same time. Useful when pointed&lt;br&gt;
outward. Corrosive when pointed inward, because the counter-argument doesn't sit next to the&lt;br&gt;
first position, it tries to erase it. Grateful for the past, but it cost me something real,&lt;br&gt;
failing a job interview among the costs. Forgiving someone, but not reconciled with them.&lt;br&gt;
The old move was picking one of those as the real feeling and discarding the other as noise.&lt;/p&gt;

&lt;p&gt;The question that actually helps isn't which one is true. Both are. The question is what&lt;br&gt;
becomes true if I stop forcing a choice between them.&lt;/p&gt;
&lt;h2&gt;
  
  
  A system that already does this
&lt;/h2&gt;

&lt;p&gt;The settlement engine I built has a state called&lt;br&gt;
&lt;code&gt;UNKNOWN&lt;/code&gt;. It's not a bug, it's a deliberate third option, used when a payment might have&lt;br&gt;
gone through or might not have, and the system genuinely doesn't have enough information yet&lt;br&gt;
to say which.&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;// Reused across three different situations: a real gateway failure,&lt;/span&gt;
&lt;span class="c1"&gt;// exhausted retries after a database deadlock, and a hard crash that&lt;/span&gt;
&lt;span class="c1"&gt;// left a settlement stuck with no way to confirm what happened.&lt;/span&gt;
&lt;span class="n"&gt;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;finalizeSettlement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GatewayResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SettlementOutcome&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UNKNOWN&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tempting design is to force an answer immediately: retry until you get a clean yes or no,&lt;br&gt;
because "unknown" feels like a failure to decide. The actual design holds &lt;code&gt;UNKNOWN&lt;/code&gt; as a&lt;br&gt;
first-class state, moves on, and lets a separate reconciliation process resolve it later,&lt;br&gt;
once there's real information to resolve it with. Forcing a premature CONFIRMED or FAILED&lt;br&gt;
would be lying to the rest of the system, papering over uncertainty that was still genuinely&lt;br&gt;
open. The honest version of "I don't know yet" turns out to be more useful than a fast, fake&lt;br&gt;
certainty. That's the same shape as forgiving without reconciling. Not every open state needs&lt;br&gt;
collapsing into a resolved one on a deadline. Some of them get resolved later, correctly,&lt;br&gt;
once there's enough to resolve them with. Some of them are just allowed to be two things at&lt;br&gt;
once, permanently, the way gratitude and grief can both be true about the same period without&lt;br&gt;
either one cancelling the other.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where this gets tested, not just described
&lt;/h2&gt;

&lt;p&gt;I've also been carrying a specific belief:&lt;br&gt;
that my anxiety comes from a need to control everything. It's a real, documented idea in&lt;br&gt;
general, intolerance of uncertainty, anxiety tracking unpredictability rather than actual&lt;br&gt;
danger, controlling behavior as an attempt to shrink that unpredictability back down. But a&lt;br&gt;
documented general pattern isn't the same as a verified account of one specific person, and&lt;br&gt;
right now I don't have an instance attached to it, just the label.&lt;/p&gt;

&lt;p&gt;There's a code parallel here too, and it's not a flattering one. The same idempotency work&lt;br&gt;
this week turned up two concurrency races, both caused by the system trying to guarantee&lt;br&gt;
too much certainty upfront.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two requests, same key, same instant.

Option A: lock everything before
either proceeds. Force certainty
immediately.

Option B: let both proceed. The
database's unique constraint decides
who wins. The loser recovers by
reading the winner's result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual code takes option B. Locking everything upfront to force immediate certainty is&lt;br&gt;
exactly what produces a deadlock later, two transactions each holding what the other one&lt;br&gt;
wants, both stuck, because the system tried to control too much of the outcome up front&lt;br&gt;
instead of accepting a controlled version of not-knowing-yet and resolving it after the&lt;br&gt;
fact. The fix that shipped this week for the crash-recovery gap follows the same shape: not&lt;br&gt;
tighter control at the moment of failure, a sweep that runs later, accepts the outcome was&lt;br&gt;
genuinely unknown for a while, and resolves it once enough time has passed to trust the&lt;br&gt;
answer.&lt;/p&gt;

&lt;p&gt;I don't know yet whether "addiction to control" is actually what's driving the anxiety, or&lt;br&gt;
just the label that was closest at hand when I needed one. That's not resolved in this&lt;br&gt;
piece, on purpose. If forcing a system to decide before it has enough information produces a&lt;br&gt;
deadlock, forcing a belief about myself into a tidy answer before I've actually tested it&lt;br&gt;
probably isn't any more reliable. The honest state to hold it in, for now, is &lt;code&gt;UNKNOWN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Two things can both be true about the same period. Stop asking which one is real.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNKNOWN&lt;/code&gt; is an honest state. A fast, fake certainty is worse than a slow, correct answer.&lt;/li&gt;
&lt;li&gt;Forcing certainty upfront is what caused the deadlocks. That applies to beliefs about
myself too.&lt;/li&gt;
&lt;li&gt;"My anxiety comes from control" stays a hypothesis until a real instance tests it.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/what-becomes-true-when-both-stay/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sapiens</category>
      <category>selfobservation</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Knowing and being able to explain are not the same skill</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:53:50 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/knowing-and-being-able-to-explain-are-not-the-same-skill-4bng</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/knowing-and-being-able-to-explain-are-not-the-same-skill-4bng</guid>
      <description>&lt;h2&gt;
  
  
  Two different skills
&lt;/h2&gt;

&lt;p&gt;There's a difference between recognizing the shape of a solution and being able to derive it&lt;br&gt;
without the shape already in front of you. The first is pattern matching. The second is&lt;br&gt;
understanding. Most technical education optimizes for the first, because it's faster to teach&lt;br&gt;
and easier to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the gap shows
&lt;/h2&gt;

&lt;p&gt;The gap only shows up when someone asks "why does it work that way" instead of "does it work."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Recognizing a solution and deriving it are separate skills, and practicing one doesn't
train the other.&lt;/li&gt;
&lt;li&gt;"Does it work?" tests recognition. "Why does it work that way?" tests understanding. Ask
myself the second one.&lt;/li&gt;
&lt;li&gt;A gap that only appears under a specific question is worth testing directly, not assuming.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/knowing-vs-explaining/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>firstprinciples</category>
    </item>
    <item>
      <title>Claim, Argument, and the Discipline of Not Fooling Yourself</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:53:19 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/claim-argument-and-the-discipline-of-not-fooling-yourself-33nm</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/claim-argument-and-the-discipline-of-not-fooling-yourself-33nm</guid>
      <description>&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;I did a set of exercises this morning to raise my energy, then showered, because I've learned&lt;br&gt;
I need to feel physically settled before my mind is any good to me. Only afterward, reading a&lt;br&gt;
chapter about hunter-gatherer instincts colliding with modern life, did I have language for&lt;br&gt;
why. The insight came after the action, not before it. That ordering turned out to matter&lt;br&gt;
more than the insight itself.&lt;/p&gt;

&lt;p&gt;Here's the trap I nearly walked into. I read a compelling argument, that a mismatch between&lt;br&gt;
old wiring and new environments explains a lot of modern unease, and I started treating it as&lt;br&gt;
settled. Not because the evidence demanded it, but because the writing was good. A well-made&lt;br&gt;
argument and a demonstrated fact produce the same feeling of certainty in a reader. That&lt;br&gt;
feeling is not evidence. It's craft.&lt;/p&gt;

&lt;h2&gt;
  
  
  The distinction that actually matters
&lt;/h2&gt;

&lt;p&gt;A claim is something shown to be true, checked against reality, falsifiable and unfalsified.&lt;br&gt;
An argument is something &lt;em&gt;made&lt;/em&gt; to be believed, structured, sequenced, persuasive by design.&lt;br&gt;
Good nonfiction is full of arguments dressed in the confidence of claims, and the better the&lt;br&gt;
writing, the harder that becomes to notice. The specific idea I was reading, that suppressed&lt;br&gt;
movement contributes to depression, is a real argument worth taking seriously. It is not a&lt;br&gt;
settled account of what causes depression, which research treats as a tangle of contributing&lt;br&gt;
factors, not a single mechanism. Losing that distinction wouldn't just cost me an accurate&lt;br&gt;
view of one book. It's the kind of thing that quietly reshapes how you read your own career&lt;br&gt;
decisions, your own dissatisfaction, your own choices, once a persuasive frame is sitting&lt;br&gt;
unexamined in the back of your head.&lt;/p&gt;

&lt;p&gt;There's a second version of the same trap, closer in: I'd summarized what I'd read, in my own&lt;br&gt;
words, and then treated my own summary as if it were a faithful account of the source. It's&lt;br&gt;
entirely possible I misread the chapter. My understanding of an idea and a verified account of&lt;br&gt;
that idea are not the same thing, and conflating them means any misreading I make gets&lt;br&gt;
laundered into something that looks like the author's authority instead of my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd already been doing this right
&lt;/h2&gt;

&lt;p&gt;The clearest working example of separating claim from argument that I have access to isn't&lt;br&gt;
philosophical at all. It's a settlement-engine system I built myself, with Claude Code, and&lt;br&gt;
am now restudying deliberately, not to relearn it from scratch, but to make sure I actually&lt;br&gt;
know it well enough to explain it, in passing or in detail, on demand. That gap, between&lt;br&gt;
having built something and being able to explain it under pressure, is one I've caught in&lt;br&gt;
myself before. This restudy is a direct test of it.&lt;/p&gt;

&lt;p&gt;What I found worth studying closely is a documentation habit built into the project itself: a&lt;br&gt;
decisions log that doesn't just record what was decided, it records &lt;em&gt;why&lt;/em&gt;, one sentence per&lt;br&gt;
decision, stated plainly enough to be checked. "Foreign key validation failures return 400,&lt;br&gt;
not 404, because the bad ID is client input error, not a missing resource being fetched&lt;br&gt;
directly." That's not a conclusion asserted with confidence. That's a reasoned claim, sitting&lt;br&gt;
next to the reasoning that produced it, inviting disagreement rather than discouraging it.&lt;/p&gt;

&lt;p&gt;Compare that to a decisions log that just says "we chose 400." Both read as authoritative if&lt;br&gt;
you don't look closely. Only one of them actually lets you check the reasoning instead of&lt;br&gt;
just trusting the confidence of the person who wrote it. That's the whole trick. Confidence is&lt;br&gt;
cheap. Showing your work is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like day to day
&lt;/h2&gt;

&lt;p&gt;In reading: hold two separate questions for anything persuasive. What is being claimed, and&lt;br&gt;
what is being argued for. Notice which one produced your certainty. If it's the second, the&lt;br&gt;
certainty is borrowed from the writing, not earned from the evidence.&lt;/p&gt;

&lt;p&gt;In relationships and self-observation: distinguish what happened from what you concluded it&lt;br&gt;
meant. A single explanation that fits doesn't mean it's the only one, or the right one, it&lt;br&gt;
means it's plausible enough to feel finished. Plausible and finished are not the same&lt;br&gt;
condition.&lt;/p&gt;

&lt;p&gt;In code, and in any documentation that describes a system: treat a decisions log entry, a&lt;br&gt;
comment, or a design doc the way you'd treat a persuasive book, with the same two questions.&lt;br&gt;
Does this line state a checkable reason, or does it just assert a conclusion with confidence?&lt;br&gt;
A codebase full of confident assertions with no reasoning attached is tunnel vision waiting to&lt;br&gt;
happen, for whoever reads it next, including the person who wrote it, six months later,&lt;br&gt;
having forgotten why. That's precisely the risk I'm restudying my own system to guard&lt;br&gt;
against, having built it doesn't guarantee I could still explain it cold.&lt;/p&gt;

&lt;p&gt;The actual discipline isn't suspicion of everything. It's narrower than that: know which of&lt;br&gt;
your beliefs are standing on demonstrated ground and which are standing on good writing, good&lt;br&gt;
confidence, or your own possibly-mistaken summary of something else. Most of the time you can&lt;br&gt;
hold both kinds of belief at once, loosely, without needing to resolve which is which&lt;br&gt;
immediately. The problem only starts when you stop checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  No tidy ending
&lt;/h2&gt;

&lt;p&gt;I don't have a tidy ending for this one, and I'm treating that as correct rather than&lt;br&gt;
unfinished. The whole point was to stop mistaking a well-argued position for a demonstrated&lt;br&gt;
one. Reaching a neat conclusion here would be exactly the thing I just spent this piece&lt;br&gt;
arguing against doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Certainty from good writing feels the same as certainty from evidence. Check which one I'm
standing on.&lt;/li&gt;
&lt;li&gt;My own summary of a source is not the source. Mark it as mine.&lt;/li&gt;
&lt;li&gt;A decision is only checkable when its reason is written next to it, in code and in life.&lt;/li&gt;
&lt;li&gt;Not every belief needs resolving now. The problem starts when I stop checking.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/claim-versus-argument/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reading</category>
      <category>reasoning</category>
      <category>documentation</category>
    </item>
    <item>
      <title>A Mind That Lies to You Too</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:52:46 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/a-mind-that-lies-to-you-too-1h69</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/a-mind-that-lies-to-you-too-1h69</guid>
      <description>&lt;h2&gt;
  
  
  What sapiens traded time for
&lt;/h2&gt;

&lt;p&gt;Every other species that reached a new environment got there the slow way: fins, blubber, a&lt;br&gt;
nose built as a snorkel, millions of years of the body itself changing to fit the place. When&lt;br&gt;
sapiens reached Australia, the body that arrived was the same one that had walked out of&lt;br&gt;
Africa. No gills, no fur suited to a new climate. What made the crossing possible was a mind&lt;br&gt;
good enough to understand the sea and build something that could cross it, in one lifetime,&lt;br&gt;
not a species-length of them.&lt;/p&gt;

&lt;p&gt;That's a real trade, not a free upgrade. Every other animal is capped by what its body allows&lt;br&gt;
it to take from an environment. A mind with a plan has no such cap, and it showed almost&lt;br&gt;
immediately. Sapiens gives the number: 23 of 24 Australian animal species over 50kg were gone&lt;br&gt;
within a few thousand years of that crossing. The local megafauna had never seen anything&lt;br&gt;
like this arrival and had no instinct to fear it. In Africa, animals evolved alongside early&lt;br&gt;
humans for close to two million years and learned caution the slow way. Everywhere sapiens&lt;br&gt;
reached quickly instead, that warning never had time to happen.&lt;/p&gt;

&lt;p&gt;The same trade explains something closer to us than a wombat the size of a hippo. Before&lt;br&gt;
sapiens spread, several other human species were alive at the same time, Neanderthals,&lt;br&gt;
Homo erectus, others. Sapiens is the only one left. Whatever let a mind out-hunt giant&lt;br&gt;
kangaroos also let it outcompete or eliminate every other kind of human. The mind that solved&lt;br&gt;
the sea didn't stop at animals.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same tool turns on you
&lt;/h2&gt;

&lt;p&gt;Here's the part that doesn't stay in the past. A mind capable of outthinking millions of&lt;br&gt;
years of evolution is not automatically trustworthy about itself. The same faculty that plans&lt;br&gt;
a boat also generates a justification for whatever you've already decided, and does it just&lt;br&gt;
as fluently. It doesn't announce which mode it's in. A conclusion produced by looking at&lt;br&gt;
evidence and a conclusion produced by protecting a belief you're attached to feel identical&lt;br&gt;
from the inside.&lt;/p&gt;

&lt;p&gt;That's not a flaw sapiens escaped by getting smarter. It's the same trade, still running. A&lt;br&gt;
mind good enough to reshape a continent is also good enough to reshape your own account of&lt;br&gt;
yourself, and it will do that for free, without being asked, unless something is actually&lt;br&gt;
checking it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A short list for checking it
&lt;/h2&gt;

&lt;p&gt;I've been trying to internalize a set of tools for exactly that, and grouping them, not as&lt;br&gt;
ten separate items but as four jobs:&lt;/p&gt;

&lt;p&gt;Catching a thought as it forms, and catching the pattern behind thoughts that keep repeating.&lt;br&gt;
One is metacognition, the other is naming your own bias, and they're the same muscle aimed at&lt;br&gt;
two grain sizes.&lt;/p&gt;

&lt;p&gt;A test to apply once something's been caught: what evidence would actually prove this wrong?&lt;br&gt;
If the honest answer is nothing, it's stopped being a belief and become something being&lt;br&gt;
protected instead.&lt;/p&gt;

&lt;p&gt;The same discipline aimed at feelings instead of thoughts: a feeling is real, but it isn't&lt;br&gt;
automatically information about the world. "This feels like it will fail" is a fact about&lt;br&gt;
right now, not a fact about the plan.&lt;/p&gt;

&lt;p&gt;And a decision-making set for anything aimed outward: every choice costs the other choices&lt;br&gt;
you didn't make, small repeated choices compound into large outcomes over time, and a good&lt;br&gt;
decision can still produce a bad outcome, so judge the decision, not just how it turned out.&lt;/p&gt;

&lt;p&gt;I don't know yet whether these four groupings hold up as anything more than a convenient way&lt;br&gt;
to remember ten words. That's not settled here on purpose. It's a framework being tried, not&lt;br&gt;
a conclusion being reported.&lt;/p&gt;
&lt;h2&gt;
  
  
  One place it got tested this week
&lt;/h2&gt;

&lt;p&gt;The clearest test so far wasn't abstract. I explained the ledger in this system's settlement&lt;br&gt;
engine cold, described it as checked double-entry bookkeeping, the kind where a balance is&lt;br&gt;
provable, not just trusted. Then I looked:&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;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;LedgerEntryRepository&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;LedgerEntry&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in the actual code ever read those entries back. The check I'd described as already&lt;br&gt;
existing didn't exist. That's falsifiability doing its job in real time, not as a concept I&lt;br&gt;
was reading about but as a belief about my own work that had a real, checkable answer, and&lt;br&gt;
the answer was no. The honest move afterward wasn't to feel bad about being wrong. It was to&lt;br&gt;
notice that the explanation had felt just as confident either way, right or wrong, which is&lt;br&gt;
exactly the warning the concept is supposed to be. Confidence was never the signal. Whether&lt;br&gt;
there was a way to be proven wrong was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A mind good enough to outthink evolution isn't automatically trustworthy about itself.
That's the same trade, not a different one.&lt;/li&gt;
&lt;li&gt;Confidence in an explanation isn't evidence for it. Whether it could be proven wrong is.&lt;/li&gt;
&lt;li&gt;The four groupings are a framework being tested, not a conclusion. One real test this week
went the way the framework predicts; that's one data point, not a pattern yet.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/philosophy/a-mind-that-lies-to-you-too/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>sapiens</category>
      <category>cognitivebias</category>
      <category>selfobservation</category>
    </item>
    <item>
      <title>The Outbox Held, the Ledger Didn't</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:42:43 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/the-outbox-held-the-ledger-didnt-k7g</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/the-outbox-held-the-ledger-didnt-k7g</guid>
      <description>&lt;h2&gt;
  
  
  Explain it cold, then check
&lt;/h2&gt;

&lt;p&gt;Same method as idempotency: explain the mechanism from memory, then check it against the&lt;br&gt;
actual code, not the other way around. The point isn't to be right on the first try. It's&lt;br&gt;
that a claim about your own system should be falsifiable. If I can't say what I'd expect to&lt;br&gt;
find in the code that would prove my explanation wrong, I'm not really explaining it, I'm&lt;br&gt;
just repeating a shape that sounds right.&lt;/p&gt;

&lt;p&gt;Two subsystems, two different results. The outbox explanation held up once checked. The&lt;br&gt;
ledger explanation didn't, not because the mechanism I described was wrong, but because the&lt;br&gt;
part of it I assumed existed simply wasn't built yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the outbox is actually for
&lt;/h2&gt;

&lt;p&gt;The instinct is to describe it as a receipt: proof you sent something, so you can resend it&lt;br&gt;
if it doesn't land. That's close, but backward. The real problem is that a database commit&lt;br&gt;
and a message published to Kafka are two separate systems, and you can't make them one atomic&lt;br&gt;
operation. Publish first, then save to the database, and a crash in between means you told&lt;br&gt;
the world something happened that your own database never recorded. Save first, then&lt;br&gt;
publish, and a crash in between means your database says it happened but nobody downstream&lt;br&gt;
ever hears about it. Either order leaves a gap where the two systems can disagree.&lt;/p&gt;

&lt;p&gt;The fix is to make the event write local, so it can share a transaction with the fact it's&lt;br&gt;
describing:&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;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SettlementExecutionRequest&lt;/span&gt; &lt;span class="nf"&gt;createPendingSettlement&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="n"&gt;settlementRepository&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;settlement&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;outboxWriter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AGGREGATE_TYPE_SETTLEMENT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;KafkaTopics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SETTLEMENT_REQUESTED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettlementRequestedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&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="n"&gt;destination&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="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&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;Same transaction, same database. The settlement and the event to announce it commit together&lt;br&gt;
or not at all. A separate process then polls for unpublished rows and sends them on, only&lt;br&gt;
marking a row done once the broker actually confirms it:&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="n"&gt;kafkaTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTopic&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAggregateId&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPayload&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;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markPublished&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that send fails, the row just sits there, unpublished, and gets tried again next poll.&lt;br&gt;
What this buys isn't proof after the fact. It's a guarantee that the event and the thing it&lt;br&gt;
describes can never come apart: if the settlement never happened, no event exists to send. If&lt;br&gt;
it did happen, the event goes out eventually, whatever crashes in between.&lt;/p&gt;
&lt;h2&gt;
  
  
  What double-entry is supposed to guarantee, and what wasn't there
&lt;/h2&gt;

&lt;p&gt;Double-entry means every transaction writes two sides, a debit somewhere and a credit&lt;br&gt;
somewhere else, and across any transaction the two must net to zero. The point of doing it&lt;br&gt;
this way is that a balance stops being something you just trust. It becomes something you can&lt;br&gt;
prove, by summing the entries that produced it.&lt;/p&gt;

&lt;p&gt;The code writes both sides correctly:&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="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;destination&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;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;ledgerEntryRepository&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LedgerEntry&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;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&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="nc"&gt;EntryType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEBIT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="n"&gt;ledgerEntryRepository&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LedgerEntry&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;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;settlementId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&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="nc"&gt;EntryType&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;settlement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;balance&lt;/code&gt; is also a stored column, mutated directly by &lt;code&gt;debit()&lt;/code&gt;/&lt;code&gt;credit()&lt;/code&gt; in that same&lt;br&gt;
method. And this is the whole repository interface for the entries just written:&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;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;LedgerEntryRepository&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;LedgerEntry&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No query beyond what comes free. Nothing anywhere sums those entries back and checks them&lt;br&gt;
against the balance. The entries were being written correctly and never read. A ledger that&lt;br&gt;
nobody checks isn't really double-entry, it's an audit trail sitting next to a number&lt;br&gt;
everyone just trusts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deciding what happens when it's wrong
&lt;/h2&gt;

&lt;p&gt;Finding the gap is the easy part. The harder question is what a mismatch should actually do&lt;br&gt;
once the system can see one.&lt;/p&gt;

&lt;p&gt;The tempting answer is to auto-correct: recompute the balance from the entries and overwrite&lt;br&gt;
it. That's the wrong incentive to build into the system. A mismatch means you don't yet know&lt;br&gt;
which number is wrong, the stored balance or an entry. Auto-fixing assumes an answer you&lt;br&gt;
don't have, and it means a real bug gets silently absorbed instead of surfaced, which is&lt;br&gt;
exactly how a system keeps producing the same kind of failure: nothing about the mismatch&lt;br&gt;
ever reaches whoever could actually fix the cause. &lt;code&gt;docs/reconciliation.md&lt;/code&gt; already states&lt;br&gt;
this as policy for a different case, mismatches with an external gateway: manual review by&lt;br&gt;
default, no silent auto-resolution. That's not a rule specific to talking to a bank. It's the&lt;br&gt;
right rule for any internal disagreement a system can detect but can't safely resolve on its&lt;br&gt;
own, so it's now written that way, a general rule with two implementations rather than one&lt;br&gt;
rule that only happens to apply once.&lt;/p&gt;

&lt;p&gt;The fix: every account gets an opening entry so its balance always equals the sum of its own&lt;br&gt;
history, and a check runs on every read. On a disagreement, it records the mismatch, once,&lt;br&gt;
durably, and still refuses to hand back a number it doesn't trust:&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;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LedgerAccount&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;)&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;computed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledgerEntryRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sumNetByAccountId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&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="nc"&gt;BigDecimal&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;account&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBalance&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;compareTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed&lt;/span&gt;&lt;span class="o"&gt;)&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="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&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;ledgerMismatchRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByAccountIdAndResolutionStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&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="no"&gt;OPEN&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ledgerMismatchRepository&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LedgerMismatch&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;LedgerInconsistencyException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&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="n"&gt;stored&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;computed&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;Resolving that record later never touches the balance either. It records that a human looked&lt;br&gt;
at it and made a decision, the same as the existing gateway-mismatch workflow. Verified&lt;br&gt;
against real data before calling it done: forty-six settlements moved through the actual API,&lt;br&gt;
every untouched account's backfilled history matched its original balance exactly, and one&lt;br&gt;
account whose balance was hand-edited to disagree with its own history was caught,&lt;br&gt;
flagged, and resolved without its balance being touched by the fix itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A claim about your own system should be falsifiable. If nothing in the code could prove
it wrong, it isn't really an explanation.&lt;/li&gt;
&lt;li&gt;The outbox isn't proof you sent something. It's a guarantee that an event and the fact it
describes can never come apart.&lt;/li&gt;
&lt;li&gt;Entries that are only ever written and never read aren't a checked ledger, whatever they're
called.&lt;/li&gt;
&lt;li&gt;When a system finds its own mistake, the honest move is to record it and ask, not to
quietly fix it and move on.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/tech/outbox-and-ledger/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>postgres</category>
      <category>kafka</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Idempotency keys, under load</title>
      <dc:creator>Miguel Shinyenyi</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:42:41 +0000</pubDate>
      <link>https://dev.to/miguel_shinyenyi_e2291c8c/idempotency-keys-under-load-1a2c</link>
      <guid>https://dev.to/miguel_shinyenyi_e2291c8c/idempotency-keys-under-load-1a2c</guid>
      <description>&lt;h2&gt;
  
  
  The textbook version
&lt;/h2&gt;

&lt;p&gt;An idempotency key is a client-generated identifier attached to a request, so that if the&lt;br&gt;
same request arrives twice, the server recognizes it and returns the original result instead&lt;br&gt;
of processing it again. That's the textbook version. The real version, the one that shows up&lt;br&gt;
once you put a system under actual concurrent load, has two more layers to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The guarantee is "answer it correctly"
&lt;/h2&gt;

&lt;p&gt;The guarantee isn't "block the duplicate," it's "answer it correctly." A retry of an&lt;br&gt;
in-flight request gets told the process is underway. A retry of a &lt;em&gt;finished&lt;/em&gt; request gets&lt;br&gt;
back the exact same result the first one got, replayed from a stored snapshot, not a fresh&lt;br&gt;
answer. That second case is the actual point. A caller retrying after a timeout isn't told&lt;br&gt;
"processing," they're told what genuinely happened.&lt;/p&gt;

&lt;p&gt;Here's the check, in the real code:&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="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findExisting&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;existing&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="nf"&gt;handleExisting&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&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;requestHash&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;executionRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createPendingSettlement&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;requestHash&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&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;DataAccessException&lt;/span&gt; &lt;span class="n"&gt;raceLost&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="n"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settlementTransactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findExisting&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="na"&gt;orElseThrow&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;raceLost&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handleExisting&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;winner&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestHash&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;
  
  
  Race one: same key, same instant
&lt;/h2&gt;

&lt;p&gt;Check-then-write always&lt;br&gt;
has a gap. Two requests can both see nothing during &lt;code&gt;findExisting&lt;/code&gt;, before either has&lt;br&gt;
written anything. Rather than closing that gap with an upfront lock, the code lets both&lt;br&gt;
proceed, and leans on a database-level unique constraint to guarantee only one insert&lt;br&gt;
survives. The other throws, and gets recovered by re-reading whoever won.&lt;/p&gt;

&lt;p&gt;What's not obvious from reading the code: that failure doesn't always look the same. Under&lt;br&gt;
Postgres, the identical race sometimes surfaces as a clean unique-constraint violation, and&lt;br&gt;
sometimes as an outright deadlock between the two competing index insertions, depending on&lt;br&gt;
timing. That's not something you'd predict from the schema. It's something you'd only find&lt;br&gt;
by actually running concurrent load against it and watching what comes back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t1  A      findExisting(key) -&amp;gt; empty
t1  B      findExisting(key) -&amp;gt; empty
t2  A      INSERT idempotency_keys
t2  B      INSERT idempotency_keys
t3  DB     unique constraint on key:
           only one INSERT wins
t3  loser  fails as EITHER
           - a unique-violation, or
           - a deadlock
             (CannotAcquireLock)
           depending on index timing
t4  loser  catch(DataAccessException)
           -&amp;gt; findExisting(key)
           -&amp;gt; return winner's result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Race two: the winner deadlocks
&lt;/h2&gt;

&lt;p&gt;Race two is a different race, on the winner. The losing threads above, in their own&lt;br&gt;
doomed transactions, are inserting into &lt;code&gt;settlements&lt;/code&gt;, which has a foreign key back to&lt;br&gt;
&lt;code&gt;idempotency_keys&lt;/code&gt;. Validating that foreign key takes a shared lock on the referenced row.&lt;br&gt;
If those losing transactions haven't rolled back yet when the winner tries to &lt;code&gt;UPDATE&lt;/code&gt; that&lt;br&gt;
same row to mark it complete, the update deadlocks against locks held for an unrelated&lt;br&gt;
reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  Winner  createPendingSettlement()
           commits
2  Loser   INSERT INTO settlements
           (FK -&amp;gt; idempotency_keys)
           takes a shared lock on
           that row
3  Loser   rolling back, lock still
           held
4  Winner  finalizeSettlement():
           UPDATE idempotency_keys
           SET status = COMPLETED
           -&amp;gt; blocked on that lock
5  DB      detects deadlock,
           aborts the UPDATE
6  Winner  catches
           TransientDataAccessException
           retries finalizeSettlement
           only (already PENDING;
           rerunning the whole flow
           would see its own key as
           a conflict)
7  Loser   rollback completes,
           lock released
8  Winner  retry succeeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five bounded retries, short backoff. If all five are exhausted, still under heavy&lt;br&gt;
contention, the system doesn't leave the settlement stuck. It falls back to a second,&lt;br&gt;
independent retry budget that finalizes the settlement as &lt;code&gt;UNKNOWN&lt;/code&gt;, the same "we genuinely&lt;br&gt;
don't know, let reconciliation resolve it later" state used when the external call itself&lt;br&gt;
fails outright. Both concurrency behaviors were found by actually running load and chaos&lt;br&gt;
tests against the system, not by design review.&lt;/p&gt;

&lt;h2&gt;
  
  
  A third failure mode: the crash
&lt;/h2&gt;

&lt;p&gt;This one was found by studying the first two closely enough to explain them. Neither race above covers what happens if the process hard-crashes between the two&lt;br&gt;
transactions entirely, after the settlement is written as &lt;code&gt;PENDING&lt;/code&gt; but before it's ever&lt;br&gt;
finalized. No thread survives to catch anything in that case, and the settlement has no&lt;br&gt;
external reference yet, since that's only set during finalization, which made it invisible&lt;br&gt;
to reconciliation's own query. The idempotency key would stay &lt;code&gt;IN_PROGRESS&lt;/code&gt; indefinitely,&lt;br&gt;
409-ing every retry, forever. That was a real, verified gap, confirmed against the actual&lt;br&gt;
repository before writing it down here, not assumed.&lt;/p&gt;

&lt;p&gt;It's fixed now. A scheduled sweep, &lt;code&gt;StalePendingSettlementSweepService&lt;/code&gt;, runs the same way&lt;br&gt;
reconciliation already does: it finds settlements stuck &lt;code&gt;PENDING&lt;/code&gt; with no external reference&lt;br&gt;
past a grace period (300 seconds by default), and finalizes each one as &lt;code&gt;UNKNOWN&lt;/code&gt;, reusing&lt;br&gt;
the exact same "we don't know, reconciliation resolves it later" path the two races above&lt;br&gt;
already fall back to. No new state-machine logic, just the same escape hatch applied to a&lt;br&gt;
case that previously had no path to it at all. Covered by its own unit and integration&lt;br&gt;
tests, the way everything else in this system is.&lt;/p&gt;

&lt;p&gt;The interesting part was never the key itself. It's every place where "the same request&lt;br&gt;
happening twice" turns out to have more than one way of actually happening, and every place&lt;br&gt;
"finished" turns out to have more than one way of actually finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;An idempotency key's job is to answer a retry correctly, not just to block it.&lt;/li&gt;
&lt;li&gt;Let the database's unique constraint decide the race instead of locking upfront.&lt;/li&gt;
&lt;li&gt;The same race can fail two different ways. Only real concurrent load showed that.&lt;/li&gt;
&lt;li&gt;Every path needs an exit to &lt;code&gt;UNKNOWN&lt;/code&gt;, including a crash nobody survives to catch.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Originally published on &lt;a href="https://miguel-shinyenyi.github.io/miguel-site/tech/idempotency-keys/" rel="noopener noreferrer"&gt;my site&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>postgres</category>
      <category>concurrency</category>
    </item>
  </channel>
</rss>
