<?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: Tomas Almeida</title>
    <description>The latest articles on DEV Community by Tomas Almeida (@tomasralmeida).</description>
    <link>https://dev.to/tomasralmeida</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F19145%2F11ccf8c8-edf9-46d1-80f6-0fb135988387.jpg</url>
      <title>DEV Community: Tomas Almeida</title>
      <link>https://dev.to/tomasralmeida</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tomasralmeida"/>
    <language>en</language>
    <item>
      <title>Why Building a Ledger Is Harder Than It Looks</title>
      <dc:creator>Tomas Almeida</dc:creator>
      <pubDate>Thu, 11 Jun 2026 16:54:47 +0000</pubDate>
      <link>https://dev.to/tomasralmeida/why-building-a-ledger-is-harder-than-it-looks-21cm</link>
      <guid>https://dev.to/tomasralmeida/why-building-a-ledger-is-harder-than-it-looks-21cm</guid>
      <description>&lt;p&gt;On August 1, 2012, Knight Capital pushed a faulty deployment to production. Within 45 minutes, its router fired more than 4 million unintended orders into the market, and the firm lost over $460 million (&lt;a href="https://www.sec.gov/news/press-release/2013-222" rel="noopener noreferrer"&gt;U.S. SEC&lt;/a&gt;, 2013). One build, one morning, one company nearly gone.&lt;/p&gt;

&lt;p&gt;Most money systems don’t fail like that. They fail quietly: a balance drifting by a cent every few thousand operations, a webhook replay that doubles a payout, a reconciliation job that “almost matches” forever.&lt;/p&gt;

&lt;p&gt;We’ve spent years building a double-entry ledger for a crypto payments system. What follows isn’t theory. It’s the set of constraints we ended up needing once the system started seeing real money, retries, and adversarial failure modes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Floating Point Breaks Finance Systems
&lt;/h2&gt;

&lt;p&gt;Floating point isn’t “slightly imprecise.” It’s structurally incompatible with exact accounting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$1.23 USD    -&amp;gt;  123
1.23 USDC     -&amp;gt;  1230000
0.5 ETH       -&amp;gt;  500000000000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule we landed on is simple: &lt;strong&gt;money is always an integer in the smallest unit of the asset.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not decimals. Not floats. Not “precision-aware” types that still round under the hood.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;numeric(38, 0)&lt;/code&gt; so fractional units cannot exist at rest. If a half-unit appears in a calculation, it fails before it becomes debt.&lt;/p&gt;

&lt;p&gt;The real complexity shows up elsewhere: fees.&lt;/p&gt;

&lt;p&gt;A 1% fee on 99,999,999 units produces a remainder that must be deterministically assigned. If you don’t decide where that remainder goes, your ledger will drift. Slowly. Invisibly. Permanently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enforcing Double-Entry at the Database Layer
&lt;/h2&gt;

&lt;p&gt;Application-level invariants don’t survive production reality.&lt;/p&gt;

&lt;p&gt;Backfills, scripts, incident patches, and “temporary” admin queries bypass them instantly.&lt;/p&gt;

&lt;p&gt;So we push the invariant into Postgres.&lt;/p&gt;

&lt;p&gt;The rule is strict:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Within a transaction, debits must equal credits per currency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it is enforced at commit time, not row time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;postings_must_balance&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;postings&lt;/span&gt;
&lt;span class="k"&gt;DEFERRABLE&lt;/span&gt; &lt;span class="k"&gt;INITIALLY&lt;/span&gt; &lt;span class="k"&gt;DEFERRED&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;enforce_balanced_postings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the trigger itself. It’s the &lt;strong&gt;deferred validation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Without deferral, you can’t validate a group of rows that doesn’t exist yet.&lt;/p&gt;

&lt;p&gt;With it, Postgres becomes a transactional accounting engine.&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0luc2VydCBwb3N0aW5ncyByb3dzXSAtLT4gQltUcmFuc2FjdGlvbiBvcGVuXVxuICBCIC0tPiBDW0RlZmVycmVkIHRyaWdnZXIgYXQgY29tbWl0XVxuICBDIC0tPiBEe0RlYml0cyA9PSBDcmVkaXRzIHBlciBjdXJyZW5jeT99XG4gIEQgLS0-fFllc3wgRVtDb21taXRdXG4gIEQgLS0-fE5vfCBGW1JlamVjdCB0cmFuc2FjdGlvbl0iLCAibWVybWFpZCI6IHsidGhlbWUiOiAibmV1dHJhbCJ9fQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0luc2VydCBwb3N0aW5ncyByb3dzXSAtLT4gQltUcmFuc2FjdGlvbiBvcGVuXVxuICBCIC0tPiBDW0RlZmVycmVkIHRyaWdnZXIgYXQgY29tbWl0XVxuICBDIC0tPiBEe0RlYml0cyA9PSBDcmVkaXRzIHBlciBjdXJyZW5jeT99XG4gIEQgLS0-fFllc3wgRVtDb21taXRdXG4gIEQgLS0-fE5vfCBGW1JlamVjdCB0cmFuc2FjdGlvbl0iLCAibWVybWFpZCI6IHsidGhlbWUiOiAibmV1dHJhbCJ9fQ%3D%3D%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Flowchart showing postings inserted inside an open transaction, then a deferred trigger at commit checking whether debits equal credits per currency, committing if they match and rejecting the transaction if they don't" width="369" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of those rare cases where “database constraint” is not defensive programming. It is the system boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Idempotency Is Not a Feature, It’s the System Model
&lt;/h2&gt;

&lt;p&gt;Every payment system is at-least-once.&lt;/p&gt;

&lt;p&gt;Not “sometimes.” Always.&lt;/p&gt;

&lt;p&gt;Stripe explicitly retries webhooks for days and may deliver duplicates by design.&lt;/p&gt;

&lt;p&gt;That means your system is not correct if retries are handled at the edge. It must be correct under duplication everywhere.&lt;/p&gt;

&lt;p&gt;We treat idempotency as a hard uniqueness rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One intent → one transaction, forever.&lt;/p&gt;
&lt;/blockquote&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0luY29taW5nIHJlcXVlc3RdIC0tPiBCe0lkZW1wb3RlbmN5IGtleSBzZWVuP31cbiAgQiAtLT58WWVzfCBDW1JldHVybiBleGlzdGluZyB0cmFuc2FjdGlvbl1cbiAgQiAtLT58Tm98IERbSW5zZXJ0IHRyYW5zYWN0aW9uXVxuICBEIC0tPiBFe1VuaXF1ZSBjb25zdHJhaW50IGhpdD99XG4gIEUgLS0-fFllc3wgQ1xuICBFIC0tPnxOb3wgRltDcmVhdGUgcG9zdGluZ3NdIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0luY29taW5nIHJlcXVlc3RdIC0tPiBCe0lkZW1wb3RlbmN5IGtleSBzZWVuP31cbiAgQiAtLT58WWVzfCBDW1JldHVybiBleGlzdGluZyB0cmFuc2FjdGlvbl1cbiAgQiAtLT58Tm98IERbSW5zZXJ0IHRyYW5zYWN0aW9uXVxuICBEIC0tPiBFe1VuaXF1ZSBjb25zdHJhaW50IGhpdD99XG4gIEUgLS0-fFllc3wgQ1xuICBFIC0tPnxOb3wgRltDcmVhdGUgcG9zdGluZ3NdIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Flowchart of an incoming request checked against seen idempotency keys: a known key returns the existing transaction, an unknown key attempts an insert, and a unique constraint conflict also resolves to the existing transaction instead of creating new postings" width="488" height="882"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real guarantee is the database index:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;(ledger_id, idempotency_key)&lt;/code&gt; is the source of truth&lt;/li&gt;
&lt;li&gt;cache is only a performance optimization&lt;/li&gt;
&lt;li&gt;concurrency collapses naturally into a single winner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two requests race, one wins the insert, the other resolves to it. No locks. No coordination layer. Just relational constraints doing what they were designed for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Append-Only Is an Audit Model, Not a Storage Choice
&lt;/h2&gt;

&lt;p&gt;The hardest habit to break is mutability.&lt;/p&gt;

&lt;p&gt;CRUD thinking says: fix incorrect rows.&lt;/p&gt;

&lt;p&gt;Ledger thinking says: &lt;strong&gt;never change history, only extend it.&lt;/strong&gt;&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBMUlxuICBBW09yaWdpbmFsIGVudHJ5XSAtLT4gQltQb3N0ZWRdXG4gIEIgLS0-IEN7RXJyb3IgZGlzY292ZXJlZH1cbiAgQyAtLT4gRFtSZXZlcnNhbCBlbnRyeV1cbiAgRCAtLT4gRVtDb3JyZWN0aXZlIGVudHJ5XSIsICJtZXJtYWlkIjogeyJ0aGVtZSI6ICJuZXV0cmFsIn19%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBMUlxuICBBW09yaWdpbmFsIGVudHJ5XSAtLT4gQltQb3N0ZWRdXG4gIEIgLS0-IEN7RXJyb3IgZGlzY292ZXJlZH1cbiAgQyAtLT4gRFtSZXZlcnNhbCBlbnRyeV1cbiAgRCAtLT4gRVtDb3JyZWN0aXZlIGVudHJ5XSIsICJtZXJtYWlkIjogeyJ0aGVtZSI6ICJuZXV0cmFsIn19%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Left-to-right flowchart of a ledger correction: an original entry is posted, an error is discovered, and instead of editing the row a reversal entry and then a corrective entry are appended" width="988" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is not aesthetic purity. It’s operational safety.&lt;/p&gt;

&lt;p&gt;If you allow updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you destroy audit trails&lt;/li&gt;
&lt;li&gt;you break reproducibility&lt;/li&gt;
&lt;li&gt;you make reconciliation non-deterministic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you forbid updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every correction becomes explicit&lt;/li&gt;
&lt;li&gt;every mistake is traceable&lt;/li&gt;
&lt;li&gt;every balance is recomputable from first principles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We enforce this at two levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application layer rejects updates/deletes&lt;/li&gt;
&lt;li&gt;database triggers enforce it as a final gate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only mutable fields are metadata (tags, references). Money fields are immutable by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exchange Rates Must Be Frozen, Not Recomputed
&lt;/h2&gt;

&lt;p&gt;Any call to “current price” inside a ledger pipeline is a correctness bug.&lt;/p&gt;

&lt;p&gt;Not a race condition. A correctness bug.&lt;/p&gt;

&lt;p&gt;Rates are captured once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exchange_rate   decimal(38, 18)
base_currency   text
quote_currency  text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And stored on every affected entry.&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW1BheW1lbnQgY29uZmlybWVkXSAtLT4gQltGZXRjaCBGWCByYXRlXVxuICBCIC0tPiBDW1dyaXRlIHJhdGUgaW50byBsZWRnZXIgZW50cmllc11cbiAgQyAtLT4gRFtEb3duc3RyZWFtIHN5c3RlbXMgdXNlIHNuYXBzaG90IG9ubHldIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW1BheW1lbnQgY29uZmlybWVkXSAtLT4gQltGZXRjaCBGWCByYXRlXVxuICBCIC0tPiBDW1dyaXRlIHJhdGUgaW50byBsZWRnZXIgZW50cmllc11cbiAgQyAtLT4gRFtEb3duc3RyZWFtIHN5c3RlbXMgdXNlIHNuYXBzaG90IG9ubHldIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Flowchart of exchange-rate snapshotting: when a payment is confirmed the FX rate is fetched once, written into the ledger entries, and every downstream system reads only that stored snapshot" width="276" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This ensures that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries don’t change accounting&lt;/li&gt;
&lt;li&gt;reports are deterministic across time&lt;/li&gt;
&lt;li&gt;historical balances remain reproducible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A refund six months later uses the original rate, not today’s market rate. That’s not “approximate fairness.” It’s consistency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reconciliation Is Where Systems Stop Agreeing With Each Other
&lt;/h2&gt;

&lt;p&gt;Everything before this point ensures internal consistency.&lt;/p&gt;

&lt;p&gt;Reconciliation is where reality enters.&lt;/p&gt;

&lt;p&gt;And reality is messy.&lt;/p&gt;

&lt;p&gt;External systems differ in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timing&lt;/li&gt;
&lt;li&gt;batching&lt;/li&gt;
&lt;li&gt;rounding rules&lt;/li&gt;
&lt;li&gt;partial settlements&lt;/li&gt;
&lt;li&gt;eventual consistency windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So reconciliation cannot be a simple equality check.&lt;/p&gt;

&lt;p&gt;It becomes a matching engine with strategies.&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0V4dGVybmFsIHJlY29yZHNdIC0tPiBCW1JlY29uY2lsaWF0aW9uIGVuZ2luZV1cbiAgQiAtLT4gQ1tFeGFjdCBtYXRjaF1cbiAgQiAtLT4gRFtXaW5kb3dlZCBzdW0gbWF0Y2hdXG4gIEIgLS0-IEVbVG9sZXJhbmNlLWJhc2VkIG1hdGNoXVxuICBDIC0tPiBGW01hcmsgcmVjb25jaWxlZF1cbiAgRCAtLT4gRlxuICBFIC0tPiBGXG4gIEIgLS0-IEdbVW5tYXRjaGVkIHF1ZXVlIGZvciByZXZpZXddIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW0V4dGVybmFsIHJlY29yZHNdIC0tPiBCW1JlY29uY2lsaWF0aW9uIGVuZ2luZV1cbiAgQiAtLT4gQ1tFeGFjdCBtYXRjaF1cbiAgQiAtLT4gRFtXaW5kb3dlZCBzdW0gbWF0Y2hdXG4gIEIgLS0-IEVbVG9sZXJhbmNlLWJhc2VkIG1hdGNoXVxuICBDIC0tPiBGW01hcmsgcmVjb25jaWxlZF1cbiAgRCAtLT4gRlxuICBFIC0tPiBGXG4gIEIgLS0-IEdbVW5tYXRjaGVkIHF1ZXVlIGZvciByZXZpZXddIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Flowchart of a reconciliation engine taking external records and trying three strategies, exact match, windowed sum match, and tolerance-based match, marking successes as reconciled and routing everything else to an unmatched queue for human review" width="1018" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key design choice: reconciliation writes back into the ledger.&lt;/p&gt;

&lt;p&gt;A posting is either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconciled&lt;/li&gt;
&lt;li&gt;tolerably auto-matched&lt;/li&gt;
&lt;li&gt;or explicitly flagged for review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No spreadsheets. No external truth tables.&lt;/p&gt;

&lt;p&gt;The ledger is the system of record, not the reconciliation output.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Minimal Model Behind a Payment Gateway
&lt;/h2&gt;

&lt;p&gt;At runtime, the entire system collapses into four accounts:&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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImdyYXBoIFREXG4gIEFbQ3J5cHRvIFJlY2VpdmVkXSAtLT4gQltNZXJjaGFudCBQYXlhYmxlXVxuICBBIC0tPiBDW0ZlZSBSZXZlbnVlXVxuICBCIC0tPiBEW1BheW91dCBEaXNidXJzZWRdIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" 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%2Fmermaid.ink%2Fimg%2FeyJjb2RlIjogImdyYXBoIFREXG4gIEFbQ3J5cHRvIFJlY2VpdmVkXSAtLT4gQltNZXJjaGFudCBQYXlhYmxlXVxuICBBIC0tPiBDW0ZlZSBSZXZlbnVlXVxuICBCIC0tPiBEW1BheW91dCBEaXNidXJzZWRdIiwgIm1lcm1haWQiOiB7InRoZW1lIjogIm5ldXRyYWwifX0%3D%3Ftype%3Dpng%26bgColor%3Dwhite" alt="Diagram of the four ledger accounts behind a payment gateway: crypto received splits into merchant payable and fee revenue, and merchant payable flows down into payout disbursed" width="411" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That simplicity is misleading.&lt;/p&gt;

&lt;p&gt;The correctness doesn’t come from the graph.&lt;/p&gt;

&lt;p&gt;It comes from everything around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;integer-only math&lt;/li&gt;
&lt;li&gt;deferred constraints&lt;/li&gt;
&lt;li&gt;idempotency at the database level&lt;/li&gt;
&lt;li&gt;append-only history&lt;/li&gt;
&lt;li&gt;frozen exchange rates&lt;/li&gt;
&lt;li&gt;reconciliation as a write-back process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those, the diagram is just wishful thinking.&lt;/p&gt;

&lt;p&gt;With them, it becomes a financial system.&lt;/p&gt;




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

&lt;p&gt;None of this is exotic. That’s the uncomfortable part.&lt;/p&gt;

&lt;p&gt;Each rule is simple in isolation. Every failure mode only appears under production pressure: retries, concurrency, partial failure, and time.&lt;/p&gt;

&lt;p&gt;The systems that survive are the ones that assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the network will duplicate requests&lt;/li&gt;
&lt;li&gt;the database is the only trustworthy boundary&lt;/li&gt;
&lt;li&gt;time will invalidate assumptions&lt;/li&gt;
&lt;li&gt;humans will eventually need to audit everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A ledger isn’t a data model.&lt;/p&gt;

&lt;p&gt;It’s a collection of enforced invariants that happen to store money.&lt;/p&gt;

&lt;p&gt;If you don’t want to build all of this from scratch, there’s also SouthPay Ledger: &lt;a href="https://southpay.io/ledger?utm_source=dev.to"&gt;SouthPay Ledger&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>postgres</category>
      <category>payments</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
