<?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: Felipe Ascari</title>
    <description>The latest articles on DEV Community by Felipe Ascari (@felipe_ascari).</description>
    <link>https://dev.to/felipe_ascari</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3603174%2Fa2ee32b0-da0f-49bf-9ad2-a56359905d4a.png</url>
      <title>DEV Community: Felipe Ascari</title>
      <link>https://dev.to/felipe_ascari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/felipe_ascari"/>
    <language>en</language>
    <item>
      <title>Fintech on Go: what the language solves in a crypto backend (Part 1)</title>
      <dc:creator>Felipe Ascari</dc:creator>
      <pubDate>Thu, 23 Apr 2026 19:39:05 +0000</pubDate>
      <link>https://dev.to/felipe_ascari/fintech-on-go-what-the-language-solves-in-a-crypto-backend-part-1-4adm</link>
      <guid>https://dev.to/felipe_ascari/fintech-on-go-what-the-language-solves-in-a-crypto-backend-part-1-4adm</guid>
      <description>&lt;p&gt;&lt;em&gt;A two-part case study on building an ERC-20 rewards service in Go, covering goroutines, replicas, and the consistency problems that surface before a transaction is signed. Part 1 tackles nonce sequencing and idempotency.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;A crypto payments backend holds state in two systems that disagree often: Postgres and the Ethereum chain. Consistency breaks at three points in an ERC-20 (Ethereum's fungible token standard) transfer. Before the broadcast, the nonce must be ordered correctly. After the broadcast, retries must be idempotent. Between the database commit and the broker that dispatches to the chain, the Transactional Outbox closes the atomicity gap. All three break because the backend is concurrent. All three resolve with the same move: coordinate outside the Go process, let Postgres be the arbiter, and make the outcome explicit in the return type. Every failure mode has a name and a type. Go's explicit error values turn financial failure cases into auditable domain objects, not hidden exceptions. Part 2 covers signing, event loops, and replay protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I wrote this
&lt;/h2&gt;

&lt;p&gt;Go is the dominant language in the backend layer of crypto fintech. Kraken, Coinbase, Zerohash, Circle, Fireblocks, and &lt;code&gt;go-ethereum&lt;/code&gt; itself run on Go. The Rust wave belongs to validators and consensus nodes, not the application layer that moves money and reconciles state.&lt;/p&gt;

&lt;p&gt;I worked on Mercado Envios while Mercado Pago was building Mercado Coin and MELI Dolar. Recurring leadership meetings on those products pulled me into studying this niche. The running example mints ERC-20 tokens across goroutines and replicas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go for this layer
&lt;/h2&gt;

&lt;p&gt;Tech leads ask this before they pick a stack, so here is the case in plain terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;go-ethereum is Go, not a binding.&lt;/strong&gt; The reference Ethereum implementation is written in Go. &lt;code&gt;ethclient.Dial&lt;/code&gt;, &lt;code&gt;types.SignTx&lt;/code&gt;, and &lt;code&gt;crypto/ecdsa&lt;/code&gt; are not wrappers around a C library or JNI bindings to a Java implementation. They are the source code you can read, audit, and step through in a debugger in the same language you ship. When behavior at the protocol level is ambiguous, reading &lt;code&gt;go-ethereum/core/types&lt;/code&gt; is the answer. Node and Java teams open a translation layer; Go teams open the same repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interfaces over go-ethereum enable testing without a live node.&lt;/strong&gt; The service defines a four-method &lt;code&gt;EthereumClient&lt;/code&gt; interface at the use case layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EthereumClient&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SendTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;SuggestGasPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&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="n"&gt;big&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;TransactionReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txHash&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&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="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;PendingNonceAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;A test replaces this with a struct returning canned receipts. No test container, no Anvil instance, no &lt;code&gt;docker-compose up&lt;/code&gt;. Go's implicit interface satisfaction means the concrete &lt;code&gt;ethclient.Client&lt;/code&gt; satisfies this interface without a single annotation. The interface lives in the use case, not next to its implementation, which is the dependency direction that keeps business logic framework-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context cancellation reaches the last provider.&lt;/strong&gt; A request walks from HTTP handler to use case to repository to the signer to one or more RPC (Remote Procedure Call) providers. Every function accepts a &lt;code&gt;context.Context&lt;/code&gt;, and a client disconnect or a 3-second deadline at the top propagates to the last &lt;code&gt;eth_getTransactionByHash&lt;/code&gt; call without glue code. Java teams emulate this with &lt;code&gt;CompletableFuture&lt;/code&gt; and thread pools, Node teams reach for &lt;code&gt;AsyncLocalStorage&lt;/code&gt; or abort controllers. In Go the cancellation is the function signature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors are domain values, not exceptions.&lt;/strong&gt; The mint use case returns a &lt;code&gt;MintResult&lt;/code&gt; struct with a &lt;code&gt;Retryable bool&lt;/code&gt; field. A caller retrying an in-flight broadcast reads that field and decides. Nothing propagates silently up a call stack. No &lt;code&gt;catch (TransientException e)&lt;/code&gt; that hides a second case. In a payment system, every failure path has a financial consequence, and Go forces you to name it at the boundary where it occurs.&lt;/p&gt;

&lt;p&gt;The concurrency primitives (goroutines, channels, errgroup) are in the language, not bolted on. 1,000 goroutines cost roughly 2MB of stack. A thread pool at the same count costs off-heap buffers and tuning. Part 2 shows the event loop that orchestrates signing and confirmation across all of this without losing work across restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes these backends different
&lt;/h2&gt;

&lt;p&gt;Two systems of record that disagree often. Postgres gives ACID and strong consistency inside its own transaction boundary. The chain gives probabilistic finality. A pending transaction can be reordered, dropped, or reorged before the receipt arrives. A confirmed receipt is still subject to short-window reorgs that senior teams handle by waiting N block confirmations. In CAP terms, Postgres is CP within the cluster and the chain is AP with eventual, probabilistic convergence. The job of the backend is not to move tokens. The job is to keep these two registers consistent under partial failure while many goroutines touch them at the same time.&lt;/p&gt;

&lt;p&gt;Three consistency problems encode that job: &lt;strong&gt;ordering&lt;/strong&gt; before the broadcast (nonce), &lt;strong&gt;resulting&lt;/strong&gt; after the broadcast (idempotency), and &lt;strong&gt;atomicity&lt;/strong&gt; across the database and the broker that feeds the chain. This article covers all three. Part 2 picks up from where atomic dispatch hands off to the live chain, covering signing, event loops, and replay protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: nonce sequencing, before the broadcast
&lt;/h2&gt;

&lt;p&gt;Every transaction from an Ethereum-style wallet carries a sequential nonce, the per-wallet counter that orders transactions from a single address. The service signs from a single hot wallet. Many goroutines issue mints in parallel, and the service runs in replicas behind a load balancer. Two goroutines that sign with the same nonce lose one of the broadcasts. A skipped nonce stalls every subsequent transaction from that wallet until an on-call engineer sends a no-op self-transfer at the missing nonce. Serialization is mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a mutex alone is not enough
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sync.Mutex&lt;/code&gt; protects state inside one process. The nonce is shared across replicas and must survive a restart, so coordination has to live outside Go's memory. Redis provides cross-process mutual exclusion with a TTL that handles crashed holders. On its own, Redis cannot detect the case where the lock expired mid-transaction and another replica moved ahead. The stale holder would still come back and commit over the new state.&lt;/p&gt;

&lt;p&gt;A fencing token, following Martin Kleppmann's argument against distributed locks used alone, closes that gap. I wrote a standalone piece on the theory at &lt;a href="https://medium.com/@felipe.ascari_49171/distributed-locks-and-fencing-tokens-31904c71f61b" rel="noopener noreferrer"&gt;distributed locks and fencing tokens&lt;/a&gt;. This section takes the theory as given and walks how it composes on top of &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkr7iizwgeiqucie9289p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkr7iizwgeiqucie9289p.png" alt="Sequence of Replica 1 and Replica 2 acquiring a Redis lock with a fencing token and committing nonce updates in Postgres only when the fence exceeds the stored value" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Redis is the outer gate. Postgres is the arbiter. The fence travels from Redis into the transaction, so a stale holder whose TTL lapsed mid-&lt;code&gt;BEGIN&lt;/code&gt; is rejected at commit time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The composition, in code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;incrementInTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fence&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gorm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walletNonceModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clauses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Locking&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Strength&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"UPDATE"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wallet_address = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FenceToken&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;ErrStaleLockToken&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentNonce&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentNonce&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FenceToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fence&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tradeoffs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Cross-replica safe&lt;/th&gt;
&lt;th&gt;Survives TTL expiry&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sync.Mutex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;In-process only, fails on restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres advisory lock&lt;/td&gt;
&lt;td&gt;Single master only&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No cross-region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis &lt;code&gt;SET NX PX&lt;/code&gt; alone&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;1 RTT&lt;/td&gt;
&lt;td&gt;Stale writer wins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis + fence + &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;1 RTT + row lock&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Chosen: correctness critical&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Failure modes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;TTL expires mid-transaction. A second replica acquires the lock, increments the fence, and commits. When the stale holder finally reaches its commit, the fence comparison returns &lt;code&gt;ErrStaleLockToken&lt;/code&gt; and the caller retries with a fresh fence.&lt;/li&gt;
&lt;li&gt;Process crashes after releasing Redis but before the database commit. The uncommitted transaction is rolled back, and the next broadcast re-reads the pre-commit nonce. No data is lost.&lt;/li&gt;
&lt;li&gt;Replicas partition from each other. Postgres is still the final arbiter. A reconciler calls &lt;code&gt;SyncFromChain(ctx, wallet, nonce)&lt;/code&gt; which upserts the nonce on &lt;code&gt;ON CONFLICT&lt;/code&gt; and heals drift.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bridge
&lt;/h2&gt;

&lt;p&gt;A correct nonce guarantees ordering before the broadcast. It does not guarantee the broadcast happens at most once. If the process dies between the RPC that sends the transaction and the database write that records the hash, a retry has no way to know the transaction already went out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: idempotent broadcast, after the broadcast
&lt;/h2&gt;

&lt;p&gt;A broadcast is a side effect with no undo. Once the signed transaction leaves the process, the mempool owns it. The backend now faces a classic dual-write problem: the local database needs to record the hash, the chain already holds the pending transaction, and the process can die in between. With goroutines retrying failed or stuck mints in parallel, two retries of the same logical transfer must not produce two broadcasts. A double broadcast credits the user twice and the treasury loses the difference. Retrying blindly double-spends. Treating every unknown state as failure loses funds. Idempotency answers this at the API surface, and a &lt;code&gt;UNIQUE&lt;/code&gt; constraint on &lt;code&gt;idempotency_key&lt;/code&gt; is the last-line backstop that refuses duplicates even when the application retries aggressively.&lt;/p&gt;

&lt;h3&gt;
  
  
  The state machine
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17vgzmyob7fbkq87i3r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17vgzmyob7fbkq87i3r3.png" alt="Idempotency state machine with Unknown, Broadcasting, Finalized, Failed, PendingStuck, and RetryableCaller states, showing transitions driven by receipt confirmation, reverts, and the thirty second timeout" width="800" height="769"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four states, one per outcome the caller needs to distinguish. The state is stored by &lt;code&gt;idempotency_key&lt;/code&gt; in the database, so the same check works across replicas and across restarts. Each state has its own explicit return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransactionStatusPending&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;MintResult&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Retryable&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Explicit results, not exceptions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MintResult&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Success&lt;/span&gt;         &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;TransactionHash&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;BlockNumber&lt;/span&gt;     &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ErrorCode&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;ErrorMessage&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Retryable&lt;/span&gt;       &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Retryable&lt;/code&gt; is the field to internalize. It is not a wrapped exception, not a magic error code, but a boolean on a result struct that maps to a gRPC response, an HTTP body, and a client that decides whether to retry. Go's insistence on explicit returns turns the four-state machine into visible, testable API surface.&lt;/p&gt;

&lt;p&gt;This is the errors-as-values pattern applied to a financial domain. Java and Kotlin throw &lt;code&gt;TransientException&lt;/code&gt; and trust the caller's catch hierarchy. Node rejects with an error object that might or might not carry a retry signal. In Go, the contract is part of the type: &lt;code&gt;Retryable bool&lt;/code&gt; is documented in the struct, enforced by the compiler, and visible in code review. Every failure case in a money system has a financial consequence. Naming them in the type system is not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure modes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The process crashes between the &lt;code&gt;SendTransaction&lt;/code&gt; RPC and the database insert. On restart, the worker queries by idempotency key, finds nothing, and starts a new broadcast. If the first RPC went through, the chain now holds a pending transaction whose hash the service does not know. A reconciler that watches the mempool by sender address heals that state.&lt;/li&gt;
&lt;li&gt;A transaction sits in &lt;code&gt;Pending&lt;/code&gt; for more than thirty seconds. The state machine transitions it to &lt;code&gt;PendingStuck&lt;/code&gt; and returns &lt;code&gt;Retryable: true&lt;/code&gt;. The caller backs off instead of holding an open request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The database is the arbiter. Not the chain, not in-process memory, not the caller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry orchestration without a framework
&lt;/h3&gt;

&lt;p&gt;The state machine answers what to do when the caller asks again. It does not by itself drive the retries. A retry loop wraps every mint call with a deadline, exponential backoff with jitter, and a context that cancels every nested RPC when the budget runs out. The entire orchestration lives in the standard library plus one &lt;code&gt;golang.org/x/sync&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;UseCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;MintWithRetries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MintResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mintOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Retryable&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;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;MintResult&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int63n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MintResult&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&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;Three things worth pointing at. &lt;code&gt;mintOnce&lt;/code&gt; carries the same &lt;code&gt;idempotency_key&lt;/code&gt; through every attempt, so the second call rejoins the state machine from wherever the first one left off. The &lt;code&gt;select { case &amp;lt;-ctx.Done() }&lt;/code&gt; block is the whole framework. A deadline from the HTTP handler cancels the timer, the timer fires and drives the next attempt, and nothing leaks once the client disconnects. And &lt;code&gt;isRetryable&lt;/code&gt; is a small predicate on domain errors, not an exception class hierarchy.&lt;/p&gt;

&lt;p&gt;Node projects installing &lt;code&gt;p-retry&lt;/code&gt; to get this surface, Java projects composing &lt;code&gt;Resilience4j&lt;/code&gt; with &lt;code&gt;CompletableFuture.orTimeout&lt;/code&gt;, and Kotlin projects stitching coroutine &lt;code&gt;withTimeout&lt;/code&gt; plus a flow operator all end up at the same shape. In Go the shape is the stdlib.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: atomicity across the database and the broker
&lt;/h2&gt;

&lt;p&gt;The idempotency pattern prevents a double-broadcast when the caller retries. It does not prevent a lost event when the process dies between two writes. The use case needs to save a domain row in Postgres and emit an event that the chain dispatcher will consume. Two systems, one logical write, and the process can die between them. Chris Richardson's Transactional Outbox pattern is the proportional answer: write the domain row and the outbound event in the same Postgres transaction, then let a separate loop publish the event asynchronously. Postgres is the source of truth. The broker is always downstream of commit, never ahead of it.&lt;/p&gt;

&lt;p&gt;The call site is one transaction boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;UseCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Credit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;CreditInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cashback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToCashback&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"save cashback: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMintRequested&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&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;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateWithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&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;&lt;code&gt;WithTransaction&lt;/code&gt; opens one Postgres transaction and threads it through the context. &lt;code&gt;Save&lt;/code&gt; and &lt;code&gt;CreateWithTx&lt;/code&gt; both land in that same transaction, and commit or rollback is a single decision. The &lt;code&gt;outbox_events&lt;/code&gt; row carries the event downstream; a relay loop picks it up out-of-band.&lt;/p&gt;

&lt;p&gt;The transaction propagation itself is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WithTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gorm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&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;The transaction rides on &lt;code&gt;context.Context&lt;/code&gt;, not as a threaded &lt;code&gt;*gorm.DB&lt;/code&gt; argument. Use cases stay GORM-unaware. Repositories read the current transaction with &lt;code&gt;DB(ctx)&lt;/code&gt; and get either the pending transaction or the pool. One code path covers both, and tests wire the same repositories against a real database without mock objects or flag switches. The pattern follows Robert Laszczak's &lt;em&gt;Database transactions in Go with layered architecture&lt;/em&gt; on threedots.tech. It is the clearest write-up of why propagating the transaction through the context pays for itself once an application has more than three repositories.&lt;/p&gt;

&lt;p&gt;The contract abstraction extends to the ERC-20 token itself. The use case defines a &lt;code&gt;TokenContract&lt;/code&gt; interface alongside &lt;code&gt;EthereumClient&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TokenContract&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Mint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransactOpts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;big&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&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="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;BalanceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallOpts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&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="n"&gt;big&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;The concrete implementation is a generated ABI binding from &lt;code&gt;abigen&lt;/code&gt;. Tests swap it for a struct that returns a deterministic transaction hash. The use case never sees the binding, never touches the ABI, and never knows whether the test runs against Anvil or a mock.&lt;/p&gt;

&lt;p&gt;The published flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcawm3isfktulp5ldaqp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcawm3isfktulp5ldaqp8.png" alt="Sequence diagram: handler writes domain row and outbox event in one Postgres transaction, outbox relay ticks and publishes to NATS then marks the row published, mint consumer applies ON CONFLICT DO NOTHING on the idempotency key" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The window Outbox closes is clearer when drawn without it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1lmc7kv9qsfsgv7gcj8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1lmc7kv9qsfsgv7gcj8.png" alt="Sequence diagram: handler commits domain row in Postgres then crashes before publishing to NATS, event is lost, user expects tokens but mint was never dispatched, leaving a manual reconciliation" width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That failure mode is not theoretical. Any dual write across a database and a broker without a coordinating pattern leaves the window open. The options for closing it all have tradeoffs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Atomic DB + broker&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Infra&lt;/th&gt;
&lt;th&gt;Fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single DB transaction only&lt;/td&gt;
&lt;td&gt;Yes (DB only)&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Breaks once the broker is involved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional Outbox&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;+1 relay tick&lt;/td&gt;
&lt;td&gt;Postgres + poller&lt;/td&gt;
&lt;td&gt;DB is source of truth, chosen here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two-phase commit (XA, eXtended Architecture)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High, blocking&lt;/td&gt;
&lt;td&gt;XA-capable broker&lt;/td&gt;
&lt;td&gt;Rarely available, high operational cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga with compensations&lt;/td&gt;
&lt;td&gt;Eventual&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;Per-step logic&lt;/td&gt;
&lt;td&gt;Complements Outbox for multi-service flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change Data Capture&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;+replication lag&lt;/td&gt;
&lt;td&gt;Debezium + Kafka&lt;/td&gt;
&lt;td&gt;Heavy infrastructure for a small team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Outbox trades one relay tick of latency for full atomicity with existing infrastructure. Two-phase commit needs an XA-capable broker that rarely exists outside legacy enterprise stacks. Saga complements Outbox when a downstream step needs compensation; the service uses &lt;code&gt;MarkFailed&lt;/code&gt; as the compensating action when retries exhaust. CDC (Change Data Capture) is the bigger-team answer for shops that already run Debezium. For a team with Postgres and any broker, Outbox is the proportional choice.&lt;/p&gt;

&lt;p&gt;Two concrete failure modes sit on either side of this choice. Without Outbox, the database commits, the publish call fails, the user is under-credited, and somebody reconciles by hand. With Outbox, the relay can crash after publishing but before marking the row published, which causes a duplicate publish on restart. The consumer absorbs it with a &lt;code&gt;UNIQUE (idempotency_key)&lt;/code&gt; constraint, which ties straight back to the idempotency pattern in Problem 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hook to part 2
&lt;/h2&gt;

&lt;p&gt;The three consistency problems are now covered. Nonce sequencing handles ordering before the broadcast. Idempotent state machines handle resulting after the broadcast. Transactional Outbox closes the gap between the database commit and the chain dispatcher. Part 2 picks up where the database hands off to the live service. It covers signing a transaction with the standard library and no C bindings, &lt;code&gt;for { select }&lt;/code&gt; event loops that do not lose work across restarts, and replay protection at the consumer end when the chain delivers the same log twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ethereum/go-ethereum" rel="noopener noreferrer"&gt;go-ethereum&lt;/a&gt;: official Go implementation of the Ethereum protocol. Source of &lt;code&gt;EthereumClient&lt;/code&gt;, &lt;code&gt;types.Transaction&lt;/code&gt;, and signer types used in code snippets.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pkg.go.dev/golang.org/x/sync" rel="noopener noreferrer"&gt;golang.org/x/sync&lt;/a&gt;: &lt;code&gt;errgroup&lt;/code&gt; and &lt;code&gt;semaphore&lt;/code&gt; packages referenced in the bounded-workers pattern.&lt;/li&gt;
&lt;li&gt;Chris Richardson, &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;Pattern: Transactional Outbox&lt;/a&gt;: foundational reference for Problem 3.&lt;/li&gt;
&lt;li&gt;Robert Laszczak, &lt;a href="https://threedots.tech/post/database-transactions-in-go/" rel="noopener noreferrer"&gt;Database transactions in Go with layered architecture&lt;/a&gt;: practical Go implementation of the transaction-through-context pattern.&lt;/li&gt;
&lt;li&gt;Martin Kleppmann, &lt;a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" rel="noopener noreferrer"&gt;How to do distributed locking&lt;/a&gt;: the original argument that motivates fencing tokens over distributed locks alone.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@felipe.ascari_49171/distributed-locks-and-fencing-tokens-31904c71f61b" rel="noopener noreferrer"&gt;Distributed locks and fencing tokens&lt;/a&gt;: the standalone piece on the theory this article applies.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/fascari/cashback-platform" rel="noopener noreferrer"&gt;fascari/cashback-platform&lt;/a&gt;: Go blockchain adapter service used as the running example throughout this series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part 1 of a two-part series on building crypto payments backends in Go. Part 2 covers signing with the standard library and event loops that do not lose work across restarts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>fintech</category>
      <category>crypto</category>
    </item>
  </channel>
</rss>
