<?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: Signing, event loops, and replay protection without an SDK (Part 2)</title>
      <dc:creator>Felipe Ascari</dc:creator>
      <pubDate>Sat, 02 May 2026 14:57:39 +0000</pubDate>
      <link>https://dev.to/felipe_ascari/fintech-on-go-signing-event-loops-and-replay-protection-without-an-sdk-part-2-47e8</link>
      <guid>https://dev.to/felipe_ascari/fintech-on-go-signing-event-loops-and-replay-protection-without-an-sdk-part-2-47e8</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of a two-part case study on building an ERC-20 rewards service in Go. This one covers stdlib signing, the event loop shape that runs the async pipelines, and replay protection at the consumer end.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Signing an on-chain transaction is two library calls. &lt;code&gt;crypto/ecdsa&lt;/code&gt; plus &lt;code&gt;go-ethereum/types&lt;/code&gt; land it in five lines.&lt;/li&gt;
&lt;li&gt;Event pipelines are one &lt;code&gt;for { select }&lt;/code&gt; over three channels. The same shape runs the deposit monitor and the reconciler.&lt;/li&gt;
&lt;li&gt;Reach for Zerohash, Fireblocks, or Circle first. Write this code only when self-custody is part of the product.&lt;/li&gt;
&lt;li&gt;Go does not give you ABI (Application Binary Interface) encoding, reorg handling, gas estimation, or HSM (Hardware Security Module) integration. Those are domain problems, not language problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap of Part 1
&lt;/h2&gt;

&lt;p&gt;Part 1 covered three consistency problems in an ERC-20 (Ethereum's fungible token standard) backend: ordering (nonce sequencing across goroutines and replicas), resulting (idempotent retries that do not double-mint), and atomicity (the Transactional Outbox pattern that keeps Postgres and the broker in agreement). How Go's explicit error values turn each failure case into a named domain object ran through all three. Part 2 picks up where atomic dispatch hands off to the live chain: signing, event loops, and replay protection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/felipe_ascari/fintech-on-go-what-the-language-solves-in-a-crypto-backend-part-1-4adm"&gt;Read Part 1: what the language solves in a crypto backend&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for an SDK
&lt;/h2&gt;

&lt;p&gt;Custody is hard, and compliance is harder. Before any of this code earns its place, the commercial alternatives deserve the opening paragraph.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;What it abstracts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zerohash&lt;/td&gt;
&lt;td&gt;Custody, signing, settlement, and compliance for fintechs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fireblocks&lt;/td&gt;
&lt;td&gt;Institutional custody with MPC (Multi-Party Computation) and a policy engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circle&lt;/td&gt;
&lt;td&gt;USDC issuance, payouts, treasury, and a wallets API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coinbase Prime&lt;/td&gt;
&lt;td&gt;Institutional custody and trading with an API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BitGo&lt;/td&gt;
&lt;td&gt;Multi-sig custody, staking, and a wallets API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An SDK (Software Development Kit) is the correct choice when crypto sits next to the core business, per-transaction fees are acceptable at the target volume, on-chain programmability requirements are shallow, and the team does not want to own signing key material. The self-custody path is correct when custody is part of the product, BaaS (Blockchain as a Service) fees stop being economic at scale, programmability exceeds what the SDK exposes, or regulation forces in-house signing. A Go service on this path ships as a single static binary. &lt;code&gt;CGO_ENABLED=0 go build&lt;/code&gt; produces an executable that runs on a &lt;code&gt;gcr.io/distroless/static&lt;/code&gt; base image. No shell, no package manager, and the container layer stays under 20MB. A Node or JVM (Java Virtual Machine) service at the same stage carries its runtime, its dependency tree, and warm-up time before the first signing call. The patterns below assume the second path, and most of them still apply to any team that writes wrappers around a BaaS response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: signing with the stdlib
&lt;/h2&gt;

&lt;p&gt;A conventional payment API looks like &lt;code&gt;stripe.Charge.Create(params)&lt;/code&gt;. The SDK handles authentication, idempotency keys, retries, and webhooks. For an on-chain transaction with self-custody, no equivalent exists. Part 1 closed with an event dispatched from the Transactional Outbox. The worker downstream of the broker signs that event and broadcasts it to the chain. The backend computes transaction bytes, signs them with the wallet's private key, and submits the result to an RPC (Remote Procedure Call) endpoint. In Go, that is five lines:&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="n"&gt;signer&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;NewLondonSigner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chainID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signed&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;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SignTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsigned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privateKey&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&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="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;"sign tx for %s: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hex&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;signed&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="n"&gt;client&lt;/span&gt;&lt;span class="o"&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signed&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;NewLondonSigner&lt;/code&gt; encodes the chain identity and selects EIP-1559 rules. &lt;code&gt;SignTx&lt;/code&gt; computes the ECDSA (Elliptic Curve Digital Signature Algorithm) signature over the RLP (Recursive Length Prefix) encoded transaction. These are native Go implementations, not bindings to a C library or a JNI (Java Native Interface) wrapper. The source is in &lt;code&gt;go-ethereum/core/types&lt;/code&gt; and readable in the same language the service ships. The returned transaction carries its signature, and its hash is the broadcast identifier the database persists. &lt;code&gt;SendTransaction&lt;/code&gt; is the RPC, and &lt;code&gt;ctx&lt;/code&gt; carries the trace ID and the deadline.&lt;/p&gt;

&lt;p&gt;Where the key lives is the real question. An environment variable is fine for development and for nothing else. Cloud KMS (Key Management Service, available as AWS KMS or Google Cloud KMS) is the right default for staging and low-volume production. &lt;code&gt;crypto/ecdsa&lt;/code&gt; does not sign against a KMS directly. The integration is a thin adapter that calls the KMS &lt;code&gt;Sign&lt;/code&gt; API and returns the &lt;code&gt;r, s, v&lt;/code&gt; tuple that the go-ethereum signer wraps. HSM or MPC is the answer at high volume, where signing latency climbs into the hundreds of milliseconds per call and key caching becomes a correctness question rather than a performance one.&lt;/p&gt;

&lt;p&gt;ECDSA runs at P99 (99th-percentile latency) under 10ms on modern hardware. Go's garbage collector does not pause the goroutine running ECDSA for the duration of a full heap scan. JVM signers without careful GC tuning can spike at the wrong moment. For a service with a broadcast SLA (Service Level Agreement), predictable latency is more valuable than raw throughput, and Go's concurrent GC delivers it without configuration.&lt;/p&gt;

&lt;p&gt;Signing itself is CPU-local and fast. The temptation is to run a goroutine per transaction. Part 1 is the reason not to. Two goroutines signing from the same wallet race on the nonce. The correct parallelism axis is across wallets, not across transactions from one wallet. That is why Part 1 and Part 2 are one series rather than two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 5: event pipelines
&lt;/h2&gt;

&lt;p&gt;Every crypto backend runs at least two long-running loops. A deposit monitor that ingests inbound transfers, and a reconciler that heals state when the database and the chain disagree. Each one must survive rolling deploys, propagate context into every downstream call, and shut down within the Kubernetes grace period. The pattern is the same in each:&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;r&lt;/span&gt; &lt;span class="n"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&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="n"&gt;ticker&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;NewTicker&lt;/span&gt;&lt;span class="p"&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;interval&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;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="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="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&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;done&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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;ticker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;C&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;tick&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="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 channels and one &lt;code&gt;select&lt;/code&gt;. &lt;code&gt;ctx.Done()&lt;/code&gt; catches external cancellation. SIGTERM (the Unix process termination signal) from Kubernetes lands here when the runtime is wired with &lt;code&gt;signal.NotifyContext&lt;/code&gt;. &lt;code&gt;r.done&lt;/code&gt; catches internal shutdown, for example a health-check failure that asks the component to exit before the pod does. &lt;code&gt;ticker.C&lt;/code&gt; drives the work cadence, and &lt;code&gt;r.tick(ctx)&lt;/code&gt; passes the context forward so downstream RPC calls inherit the deadline and the trace ID.&lt;/p&gt;

&lt;p&gt;Why not cron? Three reasons. The loop carries in-memory state across ticks, a cached block height for example, which cron cannot. The loop propagates context, which cron cannot. The loop shuts down deterministically on SIGTERM, which cron does not. A cron job that misses a tick because the previous invocation is still running is a distributed systems bug waiting to happen.&lt;/p&gt;

&lt;p&gt;Two failure modes live inside this pattern. Omitting &lt;code&gt;defer ticker.Stop()&lt;/code&gt; leaks the ticker's internal goroutine across hot reloads. Calling &lt;code&gt;r.tick(context.Background())&lt;/code&gt; instead of forwarding &lt;code&gt;ctx&lt;/code&gt; severs trace propagation and deadline cascading, which turns a single slow RPC into a stuck loop.&lt;/p&gt;

&lt;p&gt;Shutdown is handled with &lt;code&gt;sync.Once&lt;/code&gt; to make &lt;code&gt;Stop&lt;/code&gt; safe to call from multiple goroutines without closing a channel twice:&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DepositMonitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&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="n"&gt;once&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;close&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="n"&gt;quit&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;close(m.quit)&lt;/code&gt; unblocks the &lt;code&gt;case &amp;lt;-m.quit&lt;/code&gt; branch in the &lt;code&gt;for { select }&lt;/code&gt; loop. &lt;code&gt;sync.Once&lt;/code&gt; guarantees the close happens exactly once regardless of how many callers race on shutdown. The pattern composes cleanly with &lt;code&gt;os/signal.NotifyContext&lt;/code&gt;: the signal handler cancels the context, the loop exits, the caller calls &lt;code&gt;Stop&lt;/code&gt; as cleanup, and &lt;code&gt;once.Do&lt;/code&gt; is a no-op.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bounded workers when the tick fans out
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;for { select }&lt;/code&gt; loop fires every interval. A naive &lt;code&gt;tick&lt;/code&gt; walks a work list serially, which is proportional at lower volumes. A service scanning hundreds of wallets per tick needs bounded parallelism: dispatch each wallet to its own goroutine, cap the concurrency so the RPC pool does not starve, and wire shutdown to the same context that kills the outer loop. &lt;code&gt;errgroup.WithContext&lt;/code&gt; plus &lt;code&gt;semaphore.NewWeighted&lt;/code&gt; from &lt;code&gt;golang.org/x/sync&lt;/code&gt; gives that in 20 lines:&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;DepositMonitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tick&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="n"&gt;wallets&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;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActiveWallets&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;sem&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWeighted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;wallets&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;sem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&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="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&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="kt"&gt;error&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;sem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;scanWallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&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="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorContext&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="s"&gt;"tick failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight workers, one errgroup, one shared context. If the outer loop receives SIGTERM, &lt;code&gt;gctx&lt;/code&gt; cancels, &lt;code&gt;sem.Acquire&lt;/code&gt; returns immediately, in-flight workers finish the RPC call they already started and release, and &lt;code&gt;g.Wait&lt;/code&gt; returns within the Kubernetes grace period. The first worker that fails cancels the rest through the errgroup, which matches the semantics the operator wants: a systemic RPC outage stops the tick fast instead of burning budget on 200 timeouts.&lt;/p&gt;

&lt;p&gt;When does this lose to Kafka Connect, Debezium, or Flink? When the event rate dwarfs what a single process can reasonably handle, when the business already operates a streaming platform, or when the downstream consumers are polyglot. Until one of those holds, a 20-line Go worker pool beats a pipeline you do not own on ownership, operability, and on-call surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 6: replay protection and idempotent consumption
&lt;/h2&gt;

&lt;p&gt;Confirmed events from the chain are not the end of the pipeline. A deposit that triggers a credit in the database must not be credited twice when the deposit monitor restarts mid-batch, when the RPC provider replays a block, or when a reorg heals and the same log surfaces again. The contract the backend needs from the loop is at-least-once delivery with idempotent consumption. The chain gives the first half. The database enforces the second.&lt;/p&gt;

&lt;p&gt;Two primitives carry the work. A unique constraint on &lt;code&gt;(tx_hash, log_index)&lt;/code&gt; in the table that records processed events, and a persisted offset that records how far the monitor has scanned. The offset lets the loop resume after a crash without rescanning from genesis. The unique constraint lets a rescan be safe when it happens anyway.&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;DepositMonitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;consume&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;ev&lt;/span&gt; &lt;span class="n"&gt;Event&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;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;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkProcessed&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;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TxHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogIndex&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;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&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;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrAlreadyProcessed&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="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;"marking event: %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="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;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreditUser&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;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&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="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 properties fall out of this shape. &lt;code&gt;MarkProcessed&lt;/code&gt; returns &lt;code&gt;ErrAlreadyProcessed&lt;/code&gt; when the unique constraint rejects the insert, and the transaction commits a no-op instead of a double credit. The credit and the dedup row commit in the same transaction, so a partial apply is impossible. And the unique index is the source of truth, not the application cache, which means a new replica coming up cold cannot double-spend a replayed event.&lt;/p&gt;

&lt;p&gt;Why not rely on the chain to tell you what is confirmed? Because the chain does not know what your backend has already done with its confirmed events. The database is the only actor with that memory. Every other pattern in this series leans on the same principle: the chain is the source of events, the database is the source of effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Go does not give you
&lt;/h2&gt;

&lt;p&gt;The language does not know what a reorg is. It does not know that an ABI change on a contract you do not own can break decoding in production six months after deploy. It does not know that a gas price estimate from a public RPC can lag the mempool by a full block. It does not know that a signing key must never leave an HSM or a cloud KMS, and that every path that touches the private key must be audited on every pull request.&lt;/p&gt;

&lt;p&gt;It also does not hide its costs. Spring and NestJS carry request scope through &lt;code&gt;ThreadLocal&lt;/code&gt; and &lt;code&gt;AsyncLocalStorage&lt;/code&gt;, and the developer rarely sees it. Go forces &lt;code&gt;ctx context.Context&lt;/code&gt; as the first parameter of every function that touches I/O. A misplaced &lt;code&gt;context.Background()&lt;/code&gt; severs trace propagation and deadline cascading in one line. That explicitness is a virtue under load and a tax under review. A payments codebase that takes the tax seriously catches the bug before it ships.&lt;/p&gt;

&lt;p&gt;The Transactional Outbox pattern from Part 1 covers the case where the write and the publish share a database. When the operation crosses service boundaries with independent stores, Outbox is not enough, and saga becomes the next pattern to reach for. I wrote the distributed version in a separate piece on &lt;a href="https://medium.com/@felipe.ascari_49171/concurrent-transactions-in-go-saga-queues-and-ddd-aggregates-part-2-2-067961e3bae2" rel="noopener noreferrer"&gt;concurrent transactions, saga, queues, and DDD (Domain-Driven Design) aggregates&lt;/a&gt;, which pairs with the in-process patterns covered here.&lt;/p&gt;

&lt;p&gt;The stack this series leans on is Go plus Postgres plus an RPC provider. What sits above that is a design problem, not a language problem. Go makes the solutions short. It does not write them for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;p&gt;Two articles, six patterns, zero frameworks. These Go properties directly earned their place in this domain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Goroutines cheap enough to run one per monitored wallet without a thread pool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;for { select }&lt;/code&gt; as the full concurrency model for a background worker&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CGO_ENABLED=0&lt;/code&gt; for a static binary under 20MB, no runtime required&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crypto/ecdsa&lt;/code&gt; with no C bindings or JNI overhead&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context.Context&lt;/code&gt; as the single pipe for cancellation, deadlines, and trace IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is a reason to rewrite a working Java or Python service. It is a reason to start a new fintech service here.&lt;/p&gt;

&lt;p&gt;If your team is evaluating stacks for a service that signs transactions, monitors a chain, or enforces idempotency against a public RPC endpoint, the patterns above are the argument. They fit the language without adapters or base classes.&lt;/p&gt;

&lt;p&gt;There is one honest caveat. Go does not shrink the domain. Reorg handling, gas estimation under congestion, HSM integration, and contract ABI upgrades are still hard engineering problems. The language makes the implementation shorter. The domain knowledge is still yours to carry.&lt;/p&gt;

&lt;p&gt;Building something similar, or hitting a problem not covered here? Drop questions and corrections in the comments.&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;TokenContract&lt;/code&gt;, &lt;code&gt;types.SignTx&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;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;: context for the replay protection and fencing approach covered in Part 1.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.fireblocks.com/" rel="noopener noreferrer"&gt;Fireblocks MPC Wallet API&lt;/a&gt;: managed signing alternative when self-custody is not viable.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.zerohash.com/" rel="noopener noreferrer"&gt;Zerohash Developer Docs&lt;/a&gt;: regulated crypto settlement infrastructure with API, webhooks, and SDK for fintechs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.circle.com/" rel="noopener noreferrer"&gt;Circle Developer Platform&lt;/a&gt;: programmable USDC wallets and payments APIs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.cdp.coinbase.com/prime/docs/welcome" rel="noopener noreferrer"&gt;Coinbase Prime API&lt;/a&gt;: institutional custody, trading, and custody APIs for financial institutions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.bitgo.com/" rel="noopener noreferrer"&gt;BitGo&lt;/a&gt;: multi-sig custody, staking, and wallet services API.&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 2 of a two-part series on Fintech on Go. &lt;a href="https://dev.to/felipe_ascari/fintech-on-go-what-the-language-solves-in-a-crypto-backend-part-1-4adm"&gt;Part 1&lt;/a&gt; covered nonce sequencing, idempotent ERC-20 minting, and the Transactional Outbox pattern, the three consistency problems that surface before a transaction is signed and broadcast.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>fintech</category>
      <category>crypto</category>
    </item>
    <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>
