<?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: Daniel Parkson Tano</title>
    <description>The latest articles on DEV Community by Daniel Parkson Tano (@parksontano).</description>
    <link>https://dev.to/parksontano</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%2F796771%2F4c2abba3-e502-4357-8df4-21c859e248f4.jpeg</url>
      <title>DEV Community: Daniel Parkson Tano</title>
      <link>https://dev.to/parksontano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/parksontano"/>
    <language>en</language>
    <item>
      <title>Designing Fee Engines for Different Payment Rails</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:44:39 +0000</pubDate>
      <link>https://dev.to/parksontano/designing-fee-engines-for-different-payment-rails-2ef1</link>
      <guid>https://dev.to/parksontano/designing-fee-engines-for-different-payment-rails-2ef1</guid>
      <description>&lt;p&gt;Making fees transparent when providers calculate and collect them differently.&lt;/p&gt;

&lt;p&gt;Fees seem simple until a product supports several payment rails.&lt;/p&gt;

&lt;p&gt;One provider may deduct a fixed fee from the recipient amount. Another may charge the platform separately. A bank transfer may use a percentage with a minimum, while a wire transfer uses a flat charge. Some fees may be waived above a transaction threshold or after sufficient monthly activity.&lt;/p&gt;

&lt;p&gt;A single &lt;code&gt;fee&lt;/code&gt; field cannot explain all of these outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the amounts in the quote
&lt;/h2&gt;

&lt;p&gt;A payment quote should distinguish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send amount
customer fee
total sender debit
provider request amount
provider fee
recipient amount
fee currency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values may be expressed in different currencies. A fee charged in the receiving currency should not be subtracted blindly from the sending wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model who absorbs the provider fee
&lt;/h2&gt;

&lt;p&gt;Suppose the recipient should receive exactly 1,000 units and the provider deducts 50.&lt;/p&gt;

&lt;p&gt;If the platform sends 1,000, the recipient receives 950. If the product promises an exact recipient amount and the platform covers the provider fee, the provider request may need to be 1,050 while the customer quote still shows a recipient amount of 1,000.&lt;/p&gt;

&lt;p&gt;That policy should be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;provider_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recipient_amount&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fee_policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform_covers_recipient_fee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;provider_amount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;quoted_provider_fee&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not hide this adjustment in an adapter without recording the quoted and submitted values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure fee rules as data
&lt;/h2&gt;

&lt;p&gt;Fee rules change more frequently than transaction code. Store configurable values such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;percentage;&lt;/li&gt;
&lt;li&gt;flat amount;&lt;/li&gt;
&lt;li&gt;minimum fee;&lt;/li&gt;
&lt;li&gt;maximum cap;&lt;/li&gt;
&lt;li&gt;transaction waiver threshold;&lt;/li&gt;
&lt;li&gt;monthly activity threshold; and&lt;/li&gt;
&lt;li&gt;rail-specific overrides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only one applicable active configuration should exist for a given scope. Changes should be auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record fee outcomes in the ledger
&lt;/h2&gt;

&lt;p&gt;When a fee is charged, create a financial record. When it is waived, preserve the waiver reason and threshold used.&lt;/p&gt;

&lt;p&gt;For monthly maintenance, use a deterministic account-and-period reference so concurrent workers cannot charge the same month twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return concise fee information to clients
&lt;/h2&gt;

&lt;p&gt;The API should provide the values the interface needs without forcing it to recreate fee logic. If a fee or cap is zero, the client can omit it. If a fee is waived, the API can return the effective fee as zero along with short eligibility information.&lt;/p&gt;

&lt;p&gt;The backend remains authoritative for calculation; the frontend remains responsible for clear presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;A fee engine translates commercial policy and provider behavior into explicit financial amounts.&lt;/p&gt;

&lt;p&gt;The quote, sender debit, provider submission, recipient credit, and recorded fee must reconcile. If those values are not separately represented, the customer experience and financial reporting will eventually disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  LinkedIn post 12A
&lt;/h2&gt;

&lt;p&gt;“Fee” is not one number in a cross-currency payment.&lt;/p&gt;

&lt;p&gt;You may need to distinguish:&lt;/p&gt;

&lt;p&gt;• customer fee&lt;br&gt;&lt;br&gt;
• total sender debit&lt;br&gt;&lt;br&gt;
• provider fee&lt;br&gt;&lt;br&gt;
• provider request amount&lt;br&gt;&lt;br&gt;
• recipient amount&lt;br&gt;&lt;br&gt;
• fee currency&lt;/p&gt;

&lt;p&gt;When the provider deducts a fee from delivery, sending the quoted recipient amount may underpay the recipient.&lt;/p&gt;

&lt;h1&gt;
  
  
  PaymentFees #Fintech #ProductEngineering #Accounting
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Multi-Currency Wallets Without Losing Auditability</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:43:26 +0000</pubDate>
      <link>https://dev.to/parksontano/multi-currency-wallets-without-losing-auditability-ckh</link>
      <guid>https://dev.to/parksontano/multi-currency-wallets-without-losing-auditability-ckh</guid>
      <description>&lt;p&gt;Designing conversions as linked financial events instead of balance edits.&lt;/p&gt;

&lt;p&gt;Adding a currency to a wallet product is not simply adding another currency code to a transaction table.&lt;/p&gt;

&lt;p&gt;Each user can own multiple balances, every currency has different decimal rules, and conversions affect two wallets at once. Historical transactions must retain the rate used at execution, even after market or configured rates change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every user-currency pair one wallet
&lt;/h2&gt;

&lt;p&gt;A database constraint should ensure that a user has no more than one active wallet for a supported currency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&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 currency itself should come from controlled configuration containing its code, precision, funding capability, withdrawal capability, display symbol, and activation state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat conversion as a financial transaction
&lt;/h2&gt;

&lt;p&gt;A conversion should create an immutable record containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one reference;&lt;/li&gt;
&lt;li&gt;source currency and amount;&lt;/li&gt;
&lt;li&gt;destination currency and amount;&lt;/li&gt;
&lt;li&gt;applied exchange rate;&lt;/li&gt;
&lt;li&gt;rate source;&lt;/li&gt;
&lt;li&gt;relevant country or market context; and&lt;/li&gt;
&lt;li&gt;execution time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inside one database transaction, lock the source and destination wallets in a consistent order, debit the source, credit the destination, and write both ledger effects.&lt;/p&gt;

&lt;p&gt;Consistent lock ordering reduces deadlock risk when concurrent conversions involve the same currency pair in opposite directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshot the applied rate
&lt;/h2&gt;

&lt;p&gt;Never reconstruct a completed conversion using the current exchange rate. The historical record must preserve the rate that produced the destination amount.&lt;/p&gt;

&lt;p&gt;If staff can configure an override rate, preserve an audit history showing the previous value, new value, actor, timestamp, and reason. Existing conversions remain unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use decimal arithmetic deliberately
&lt;/h2&gt;

&lt;p&gt;Floating-point arithmetic is inappropriate for wallet balances. Use decimal values and define rounding at the business boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wallet storage precision;&lt;/li&gt;
&lt;li&gt;quote display precision;&lt;/li&gt;
&lt;li&gt;provider submission precision; and&lt;/li&gt;
&lt;li&gt;ledger settlement precision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Round once at the required boundary, then persist the value used. Repeated rounding at multiple layers creates unexplained differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the legacy interface separate
&lt;/h2&gt;

&lt;p&gt;When migrating from one balance field to multi-currency wallets, older clients may still expect a primary account balance. A compatibility property can expose the designated primary wallet without making the legacy column authoritative again.&lt;/p&gt;

&lt;p&gt;Migration code should be explicit, reversible where practical, and followed by constraints that prevent the old and new systems from drifting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;A multi-currency wallet is a collection of currency-specific ledgers connected by immutable conversion records.&lt;/p&gt;

&lt;p&gt;The rate, rounding decision, and two wallet effects are part of one financial event. Preserving them together keeps historical balances explainable as currencies and pricing rules evolve.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Securing a Mobile Financial API in Layers</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:43:14 +0000</pubDate>
      <link>https://dev.to/parksontano/securing-a-mobile-financial-api-in-layers-145p</link>
      <guid>https://dev.to/parksontano/securing-a-mobile-financial-api-in-layers-145p</guid>
      <description>&lt;p&gt;Why authentication is only one part of protecting mobile payment traffic.&lt;/p&gt;

&lt;p&gt;A valid access token proves that a credential was accepted. It does not prove that the request is fresh, came from the expected application, originated from a trusted device, or has not been automated at an abusive rate.&lt;/p&gt;

&lt;p&gt;Mobile financial APIs need defense in depth. Each layer answers a different security question, and no single signal should carry more confidence than it deserves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the request context
&lt;/h2&gt;

&lt;p&gt;Before business logic runs, the gateway can examine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client application type;&lt;/li&gt;
&lt;li&gt;application version and build;&lt;/li&gt;
&lt;li&gt;device identifier format;&lt;/li&gt;
&lt;li&gt;content type for mutations;&lt;/li&gt;
&lt;li&gt;timestamp and nonce;&lt;/li&gt;
&lt;li&gt;request signature;&lt;/li&gt;
&lt;li&gt;IP and device reputation; and&lt;/li&gt;
&lt;li&gt;request rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks should produce clear internal reasons while returning restrained customer-facing errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign the request, not only the session
&lt;/h2&gt;

&lt;p&gt;An HMAC signature can bind the HTTP method, path, timestamp, nonce, and request body to a shared application secret or derived signing key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signature = HMAC(method + path + timestamp + nonce + body_hash)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server independently reconstructs the signed content and uses constant-time comparison. A valid signature helps detect body modification and requests created outside the expected client.&lt;/p&gt;

&lt;p&gt;Exact signing rules and enforcement thresholds should remain private.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent replay
&lt;/h2&gt;

&lt;p&gt;A correctly signed request can still be dangerous if captured and submitted again.&lt;/p&gt;

&lt;p&gt;Replay protection requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a limited timestamp window;&lt;/li&gt;
&lt;li&gt;a nonce unique within that window; and&lt;/li&gt;
&lt;li&gt;an atomic cache write that rejects an existing nonce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Financial endpoints must still be idempotent because replay protection and transaction idempotency defend different boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate-limit atomically
&lt;/h2&gt;

&lt;p&gt;A rate limiter that performs &lt;code&gt;get&lt;/code&gt;, then increments, then &lt;code&gt;set&lt;/code&gt; can lose updates across concurrent workers. An atomic cache increment produces a reliable count for each IP and device window.&lt;/p&gt;

&lt;p&gt;Limits can adapt to device reputation, but exact thresholds and risk weights should not be published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine signals carefully
&lt;/h2&gt;

&lt;p&gt;Device properties, unusual request patterns, outdated clients, and reputation can contribute to a composite risk decision. Treat weak signals as risk contributions rather than absolute proof of compromise.&lt;/p&gt;

&lt;p&gt;The enforcement model may allow, warn, block, or temporarily blacklist. False positives need an operational recovery path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the hot path efficient
&lt;/h2&gt;

&lt;p&gt;Security middleware runs on every request. Batch cache reads, short-lived in-process caching for clean devices, bounded background persistence, and guarded debug logging can reduce avoidable overhead.&lt;/p&gt;

&lt;p&gt;Performance optimizations must never cache a blocked decision incorrectly. Any blacklist or high-risk event should invalidate the local clean-device cache immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Authentication identifies a session. Layered request security evaluates whether each request is structurally valid, fresh, appropriately signed, correctly routed, and behaviorally acceptable.&lt;/p&gt;

&lt;p&gt;The strongest design combines independent controls while keeping business endpoints idempotent and the security hot path observable.&lt;/p&gt;

</description>
      <category>api</category>
      <category>cybersecurity</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>Tracking Payment Liquidity with FIFO Funding Lots</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:42:44 +0000</pubDate>
      <link>https://dev.to/parksontano/tracking-payment-liquidity-with-fifo-funding-lots-5693</link>
      <guid>https://dev.to/parksontano/tracking-payment-liquidity-with-fifo-funding-lots-5693</guid>
      <description>&lt;p&gt;Moving beyond aggregate balances to understand the real cost of every payout.&lt;/p&gt;

&lt;p&gt;A payout account may show enough money to complete a transfer, but that balance alone cannot explain what the transfer costs.&lt;/p&gt;

&lt;p&gt;Liquidity is often acquired in separate batches. Each batch may have a different exchange rate, funding date, provider cost, or settlement source. When a payout consumes that liquidity, finance teams need to know which funds were used and what margin remained.&lt;/p&gt;

&lt;p&gt;An aggregate balance answers “how much is available?” A funding-lot ledger answers “where did it come from, what did it cost, and which transactions consumed it?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Represent liquidity as inventory
&lt;/h2&gt;

&lt;p&gt;A funding lot can contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reference
funding date
original amount
remaining amount
acquisition rate
status
source metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The remaining amount changes as payouts consume the lot, while the original amount and acquisition rate remain part of the audit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Allocate oldest liquidity first
&lt;/h2&gt;

&lt;p&gt;FIFO allocation is easy to describe and subtle to implement. One payout may consume the remainder of one lot and continue into the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;lots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lock_open_lots&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;funded_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payout_amount&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lot&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lots&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;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;create_allocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reference&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;used&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining_amount&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;
        &lt;span class="n"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Row locks prevent two payouts from consuming the same remaining liquidity concurrently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make allocation retry-safe
&lt;/h2&gt;

&lt;p&gt;Before allocating, calculate how much has already been assigned to the transaction reference. A retried task should allocate only the difference.&lt;/p&gt;

&lt;p&gt;The database should also prevent the same transaction-lot relationship from being inserted twice accidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distribute financial values across lots
&lt;/h2&gt;

&lt;p&gt;If one payout spans multiple lots, its revenue and provider fee must be distributed proportionally. Assigning the full revenue or fee to each allocation overstates the transaction.&lt;/p&gt;

&lt;p&gt;For each allocation, calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;share of the total payout;&lt;/li&gt;
&lt;li&gt;attributed customer revenue;&lt;/li&gt;
&lt;li&gt;provider-fee share;&lt;/li&gt;
&lt;li&gt;cost at the lot’s acquisition rate;&lt;/li&gt;
&lt;li&gt;payout cost excluding provider fee; and&lt;/li&gt;
&lt;li&gt;resulting FX margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final allocation should absorb rounding differences so the parts equal the original transaction totals exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile derived values
&lt;/h2&gt;

&lt;p&gt;Financial rules evolve. A reconciliation service should be able to rebuild remaining lot balances and derived allocation values from authoritative transactions without deleting valid live history.&lt;/p&gt;

&lt;p&gt;Manual overrides must be explicit and traceable. A corrected revenue value should be marked as an override rather than becoming indistinguishable from a calculated value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Liquidity is financial inventory. Funding lots and immutable allocations make its movement explainable.&lt;/p&gt;

&lt;p&gt;This structure supports treasury visibility, transaction-level cost attribution, controlled reconciliation, and reliable margin analysis without relying on today’s aggregate balance to reconstruct yesterday’s economics.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Combining Automated Payment Rails with Human Fallback</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:23:47 +0000</pubDate>
      <link>https://dev.to/parksontano/combining-automated-payment-rails-with-human-fallback-2o80</link>
      <guid>https://dev.to/parksontano/combining-automated-payment-rails-with-human-fallback-2o80</guid>
      <description>&lt;p&gt;Designing one transaction lifecycle that can move safely between API-based payouts and operational fulfillment.&lt;/p&gt;

&lt;p&gt;Payment coverage is rarely uniform. One destination may support an immediate API payout. Another may require a human agent. An automated rail may also reject a transaction after the customer has already committed funds.&lt;/p&gt;

&lt;p&gt;Treating automated and manual fulfillment as unrelated products creates duplicated logic and fragmented transaction history. A better design represents them as different fulfillment strategies inside one customer-facing lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one authoritative transaction
&lt;/h2&gt;

&lt;p&gt;The customer should create one transfer with one reference, quote, recipient, funding record, and status history.&lt;/p&gt;

&lt;p&gt;The routing layer then selects a fulfillment strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;automated_rail_supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;submit_automated_payout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;queue_for_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route used should be persisted on the transaction. A later configuration change must not obscure how an existing transfer was fulfilled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define when fallback is safe
&lt;/h2&gt;

&lt;p&gt;Fallback should not occur after every provider error.&lt;/p&gt;

&lt;p&gt;The system must distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;not submitted&lt;/strong&gt;: no external payment was created, so fallback may be safe;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;definitively rejected&lt;/strong&gt;: the provider confirms no payout will occur;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;unknown outcome&lt;/strong&gt;: a timeout occurred and the provider may still process the request;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;processing&lt;/strong&gt;: an external payment exists and must be reconciled;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completed&lt;/strong&gt;: terminal success; fallback is forbidden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Falling back during an unknown outcome can pay the recipient twice. Reconciliation must resolve ambiguity before a second rail is allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the financial contract
&lt;/h2&gt;

&lt;p&gt;Changing fulfillment strategy must not silently change what the customer authorized.&lt;/p&gt;

&lt;p&gt;The transaction should retain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sender debit;&lt;/li&gt;
&lt;li&gt;recipient amount;&lt;/li&gt;
&lt;li&gt;quoted exchange rate;&lt;/li&gt;
&lt;li&gt;customer fee;&lt;/li&gt;
&lt;li&gt;payout method;&lt;/li&gt;
&lt;li&gt;recipient destination; and&lt;/li&gt;
&lt;li&gt;funding status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent may use a different internal settlement rate, but that operational rate should not rewrite the customer’s original quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assign manual work transactionally
&lt;/h2&gt;

&lt;p&gt;When a transfer enters an agent queue, competing workers or agents must not claim it simultaneously.&lt;/p&gt;

&lt;p&gt;The assignment path should lock the order, verify that it is still eligible, select an available agent, write the assignment, and create the queue record atomically.&lt;/p&gt;

&lt;p&gt;Agent selection can consider capability, destination, payout method, availability, and current workload. Exact scoring rules should remain internal because they are operational and potentially security-sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coordinate linked transactions
&lt;/h2&gt;

&lt;p&gt;Sometimes a manual fulfillment record is linked to another product record, such as a virtual-account withdrawal. One record represents the customer request; the other represents the payout workflow.&lt;/p&gt;

&lt;p&gt;The architecture must define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which record owns the funds reservation;&lt;/li&gt;
&lt;li&gt;which action confirms that payment was sent;&lt;/li&gt;
&lt;li&gt;which action completes the customer request;&lt;/li&gt;
&lt;li&gt;how cancellation propagates; and&lt;/li&gt;
&lt;li&gt;how duplicate completion is prevented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A linked transaction should be completed through an idempotent service, not through independent status assignments on both records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep staff intervention auditable
&lt;/h2&gt;

&lt;p&gt;Staff may need to confirm a payment when an agent application is unavailable or a provider integration fails. This action should require explicit permission and record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the staff actor;&lt;/li&gt;
&lt;li&gt;completion timestamp;&lt;/li&gt;
&lt;li&gt;reason or note;&lt;/li&gt;
&lt;li&gt;optional evidence;&lt;/li&gt;
&lt;li&gt;affected transaction references; and&lt;/li&gt;
&lt;li&gt;whether linked records were updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operation should lock the transaction and return safely if it was already completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notify from the authoritative transition
&lt;/h2&gt;

&lt;p&gt;Customer and agent notifications should be triggered by the service that owns the state transition. If each interface sends its own message, retries can create duplicate or contradictory notifications.&lt;/p&gt;

&lt;p&gt;Notifications are secondary effects. A notification failure should not roll back an already valid financial completion, but it should be observable and retryable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Automation and human operations can share one reliable transaction model.&lt;/p&gt;

&lt;p&gt;The routing decision selects how the obligation will be fulfilled. It does not create a second customer promise. Safe fallback requires certainty about the first rail, preserved financial terms, transactional assignment, linked-state coordination, and an auditable operational path.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Separating Available, Held, Actual, and Displayed Balances</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:23:33 +0000</pubDate>
      <link>https://dev.to/parksontano/separating-available-held-actual-and-displayed-balances-521c</link>
      <guid>https://dev.to/parksontano/separating-available-held-actual-and-displayed-balances-521c</guid>
      <description>&lt;p&gt;Why one balance field cannot represent every financial truth in a virtual-account product.&lt;/p&gt;

&lt;p&gt;“What is the balance?” sounds like a simple question until the system includes pending deposits, withdrawals awaiting completion, provider-reported funds, fees, manual reconciliation, and temporary account restrictions.&lt;/p&gt;

&lt;p&gt;These values describe different truths. Compressing them into one number makes failures difficult to recover and discrepancies difficult to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define each balance explicitly
&lt;/h2&gt;

&lt;p&gt;A virtual-account system may need at least four concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ledger balance&lt;/strong&gt;: the result of locally applied credits and debits;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;held amount&lt;/strong&gt;: funds reserved for operations that have not completed;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;available balance&lt;/strong&gt;: the amount that can be spent now;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;actual balance&lt;/strong&gt;: the latest balance reported by the external account provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some products may also maintain a &lt;strong&gt;displayed balance&lt;/strong&gt; when customer presentation must be separated from internal debt or reconciliation handling.&lt;/p&gt;

&lt;p&gt;The relationship can be expressed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available balance = ledger balance - active holds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider-reported actual balance should not overwrite the local ledger automatically. A difference may indicate pending events, fees, reversals, or a reconciliation issue that needs investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use holds for asynchronous withdrawals
&lt;/h2&gt;

&lt;p&gt;When a customer requests a withdrawal, the platform may need to reserve funds before the external payment reaches a terminal state.&lt;/p&gt;

&lt;p&gt;Immediately applying a final debit creates ambiguity if the provider later rejects the request. Leaving the full balance spendable creates an overspending risk.&lt;/p&gt;

&lt;p&gt;A hold provides the intermediate state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lock_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_id&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;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;total_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientFunds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hold_amount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;total_required&lt;/span&gt;
    &lt;span class="nf"&gt;record_hold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, consume the hold and create the final debit. On failure or cancellation, release it. Each action should use a stable transaction key so retries remain safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve before-and-after values
&lt;/h2&gt;

&lt;p&gt;Every balance mutation should record its effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reference
direction
amount
balance before
balance after
transaction type
metadata
timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ledger then explains both the current customer balance and the path used to reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat provider balances as reconciliation evidence
&lt;/h2&gt;

&lt;p&gt;External balance updates are valuable but may arrive on a different timeline from individual transaction events.&lt;/p&gt;

&lt;p&gt;The platform can store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;latest provider-reported balance;&lt;/li&gt;
&lt;li&gt;time of the last successful sync;&lt;/li&gt;
&lt;li&gt;local ledger balance; and&lt;/li&gt;
&lt;li&gt;calculated difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Staff can then investigate discrepancies without silently changing customer funds.&lt;/p&gt;

&lt;p&gt;If an authorized adjustment is required, it should be represented by a reconciliation entry with a reason, actor, reference, and before-and-after values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep fees in the ledger
&lt;/h2&gt;

&lt;p&gt;Transaction fees and monthly maintenance charges are financial events. They should not be hidden arithmetic applied only when the balance is serialized.&lt;/p&gt;

&lt;p&gt;A fee engine can determine whether a fee applies based on transaction rail, amount, account configuration, or prior-period activity. The resulting charge or exemption should be recorded so staff and customers can understand what occurred.&lt;/p&gt;

&lt;p&gt;For recurring fees, use a deterministic reference per account and billing period. This prevents two workers from charging the same month twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model temporary restrictions separately
&lt;/h2&gt;

&lt;p&gt;Sometimes an account should accept deposits but temporarily block outgoing transfers. That is an account capability decision, not a negative balance and not necessarily a debt.&lt;/p&gt;

&lt;p&gt;Explicit flags or account states such as deposit-only, outgoing hold, or suspended provide clearer behavior than manipulating the balance to force withdrawals to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Balances are views of a ledger under specific rules.&lt;/p&gt;

&lt;p&gt;Available, held, actual, and displayed amounts answer different questions. Modeling them explicitly improves withdrawal safety, reconciliation, fee transparency, and operational control without losing the audit trail behind customer funds.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Building a Provider-Agnostic Payout Architecture</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:23:18 +0000</pubDate>
      <link>https://dev.to/parksontano/building-a-provider-agnostic-payout-architecture-5hd2</link>
      <guid>https://dev.to/parksontano/building-a-provider-agnostic-payout-architecture-5hd2</guid>
      <description>&lt;p&gt;Keeping the customer experience stable while payment rails, bank directories, fees, and status semantics change underneath it.&lt;/p&gt;

&lt;p&gt;The first integration with a payment provider often becomes tightly coupled to the product. Provider-specific field names enter database models, bank codes are assumed to be universal, and technical errors flow directly to customers.&lt;/p&gt;

&lt;p&gt;The second provider exposes every one of those assumptions.&lt;/p&gt;

&lt;p&gt;A provider-agnostic architecture does not pretend all providers are identical. It gives the application a stable domain contract while allowing each adapter to preserve the differences that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate product intent from provider commands
&lt;/h2&gt;

&lt;p&gt;The product wants to perform operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;list supported banks;&lt;/li&gt;
&lt;li&gt;resolve an account;&lt;/li&gt;
&lt;li&gt;initiate a bank payout;&lt;/li&gt;
&lt;li&gt;retrieve a transaction status; and&lt;/li&gt;
&lt;li&gt;create deposit instructions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The provider adapter translates those operations into the external API’s authentication, paths, payloads, signatures, and response structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PayoutProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_banks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;narration&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_reference&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application services depend on this contract instead of importing a provider client directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep collection and disbursement configurable independently
&lt;/h2&gt;

&lt;p&gt;The best provider for accepting deposits may not be the best provider for payouts. Operational issues may affect one direction without affecting the other.&lt;/p&gt;

&lt;p&gt;Separate configuration for collection and disbursement gives operations a controlled switch without forcing a release or changing the customer flow.&lt;/p&gt;

&lt;p&gt;The chosen provider should be written onto every transaction. Current configuration selects new work; it should not rewrite history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat bank directories as provider-owned
&lt;/h2&gt;

&lt;p&gt;Bank names and codes are not always universal across providers. Two providers may represent the same institution differently.&lt;/p&gt;

&lt;p&gt;The bank selected during account creation should therefore retain its provider context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recipient account
  account number
  account name
  provider
  provider bank code
  provider bank name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the active payout provider changes, accounts created with another provider should not be silently submitted using incompatible identifiers. The product can filter saved recipients by provider or ask the customer to validate a new provider-specific destination.&lt;/p&gt;

&lt;p&gt;This is safer than guessing that similar names or codes refer to the same institution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache directories with provider-specific keys
&lt;/h2&gt;

&lt;p&gt;Bank lists are read frequently and change less often than transaction state, so caching is useful. The cache key must include the provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bank-directory:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, switching providers can continue serving the previous provider’s list until the shared cache expires. A controlled force-refresh action is also useful for operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize statuses, preserve raw responses
&lt;/h2&gt;

&lt;p&gt;Providers use different status vocabularies. The adapter should expose a small internal state while preserving the raw response for staff diagnostics.&lt;/p&gt;

&lt;p&gt;The same applies to errors. Customer-facing responses should use stable product language. Internal logs and transaction metadata can retain the exact provider error under access control.&lt;/p&gt;

&lt;p&gt;This prevents technical messages, provider account restrictions, and internal liquidity information from leaking to customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model provider-specific fee behavior explicitly
&lt;/h2&gt;

&lt;p&gt;One rail may deduct its fee from the amount delivered. Another may debit the fee separately. A common fee field does not guarantee common economic behavior.&lt;/p&gt;

&lt;p&gt;The transaction should distinguish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;amount the customer sends;&lt;/li&gt;
&lt;li&gt;platform fee charged to the customer;&lt;/li&gt;
&lt;li&gt;amount requested from the provider;&lt;/li&gt;
&lt;li&gt;provider fee;&lt;/li&gt;
&lt;li&gt;expected recipient amount; and&lt;/li&gt;
&lt;li&gt;actual recipient amount when known.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provider adapters can then construct the correct payout amount without changing how the application quotes the transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Provider independence comes from isolating differences, not erasing them.&lt;/p&gt;

&lt;p&gt;The domain layer defines the stable customer and transaction contract. Adapters own provider authentication and semantics. Transactions preserve the rail actually used. Provider-owned reference data stays scoped to that rail.&lt;/p&gt;

&lt;p&gt;With those boundaries, switching providers becomes an operational decision rather than a rewrite of the payment product.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Reconciling Payments When Webhooks Never Arrive</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:22:24 +0000</pubDate>
      <link>https://dev.to/parksontano/reconciling-payments-when-webhooks-never-arrive-5b1m</link>
      <guid>https://dev.to/parksontano/reconciling-payments-when-webhooks-never-arrive-5b1m</guid>
      <description>&lt;p&gt;Designing a recovery path for asynchronous transactions that remain stuck in processing.&lt;/p&gt;

&lt;p&gt;Webhooks provide a fast way to learn that an external payment changed state. They do not guarantee that every event will reach the application.&lt;/p&gt;

&lt;p&gt;A callback can be delayed by a network failure, rejected because of a signature or configuration problem, or lost during an outage. The provider may complete the payment while the local transaction remains in processing.&lt;/p&gt;

&lt;p&gt;Without a second source of confirmation, the transaction can remain stuck indefinitely even though the money has already moved.&lt;/p&gt;

&lt;p&gt;Reliable payment systems therefore need active reconciliation in addition to passive webhook handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record enough information to ask again
&lt;/h2&gt;

&lt;p&gt;Reconciliation starts when the transaction is created. The local record should preserve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the internal business reference;&lt;/li&gt;
&lt;li&gt;the provider used for this particular attempt;&lt;/li&gt;
&lt;li&gt;the provider transaction identifier;&lt;/li&gt;
&lt;li&gt;the current normalized state;&lt;/li&gt;
&lt;li&gt;the request and response timestamps; and&lt;/li&gt;
&lt;li&gt;a sanitized provider-response summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recorded provider matters. Querying whichever provider is currently configured can produce an incorrect result after the platform switches rails. A processing transaction must always be reconciled against the provider that originally accepted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile outside the customer request
&lt;/h2&gt;

&lt;p&gt;Opening a transaction history screen should not block while the server calls several external providers. Instead, the API can return the current local state and enqueue a background refresh for eligible processing transactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_status_refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lock_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status-refresh:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="n"&gt;reconcile_payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The short-lived cache lock prevents repeated screen refreshes from creating a burst of identical provider calls. The worker removes the lock after processing so a later request can check again if the payment remains unresolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use one normalization path
&lt;/h2&gt;

&lt;p&gt;Webhook processing and active reconciliation should not maintain separate definitions of success.&lt;/p&gt;

&lt;p&gt;Both paths should translate the external result into the same internal vocabulary and call the same guarded transition service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_provider_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;external_result&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;normalized&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;complete_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;fail_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;keep_processing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids a dangerous situation where a webhook considers one status successful while the polling worker considers it pending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bound retries without inventing failure
&lt;/h2&gt;

&lt;p&gt;A provider can continue returning pending for a legitimate transaction. After several checks, the worker may stop retrying automatically, but it should not convert uncertainty into failure without evidence.&lt;/p&gt;

&lt;p&gt;Useful outcomes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;terminal success: complete the local transaction once;&lt;/li&gt;
&lt;li&gt;terminal failure: apply the defined failure or refund path once;&lt;/li&gt;
&lt;li&gt;still processing: retry with delay up to a defined limit;&lt;/li&gt;
&lt;li&gt;unavailable provider: record the operational error and retry later;&lt;/li&gt;
&lt;li&gt;unknown status: preserve processing and flag the transaction for review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retry exhaustion means automated reconciliation paused. It does not prove that the payment failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make reconciliation safe to repeat
&lt;/h2&gt;

&lt;p&gt;The provider may send the missing webhook while the status worker is running. Both paths can discover the same terminal result at nearly the same time.&lt;/p&gt;

&lt;p&gt;The final transition must therefore lock the transaction and protect its financial side effect with an idempotency key. Reconciliation should be safe to run repeatedly, including from a staff action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give operations controlled visibility
&lt;/h2&gt;

&lt;p&gt;Some transactions will require manual investigation. Staff need access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local and provider references;&lt;/li&gt;
&lt;li&gt;the provider recorded on the transaction;&lt;/li&gt;
&lt;li&gt;the latest normalized and external statuses;&lt;/li&gt;
&lt;li&gt;timestamps for prior checks;&lt;/li&gt;
&lt;li&gt;the last safe provider error; and&lt;/li&gt;
&lt;li&gt;an action to request another status refresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provider errors should remain internal. Customers need a stable, understandable transaction state rather than raw infrastructure messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Webhooks are the low-latency path. Reconciliation is the completeness path.&lt;/p&gt;

&lt;p&gt;Using both creates a convergent lifecycle: whether the platform learns the result through a callback, a background poll, or an authorized staff check, the same protected transition brings the transaction to the same local state.&lt;/p&gt;

&lt;p&gt;This does not make external systems reliable. It makes their unreliability manageable.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Idempotency Beyond the HTTP Header</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:16:17 +0000</pubDate>
      <link>https://dev.to/parksontano/idempotency-beyond-the-http-header-9pa</link>
      <guid>https://dev.to/parksontano/idempotency-beyond-the-http-header-9pa</guid>
      <description>&lt;p&gt;Designing retry-safe financial operations across clients, workers, databases, and external providers.&lt;/p&gt;

&lt;p&gt;Payment clients retry requests. Load balancers retry requests. Background workers retry tasks. Users press a button again when the screen appears stuck. Providers may time out after accepting an operation, leaving the caller unsure whether it succeeded.&lt;/p&gt;

&lt;p&gt;Retries are normal. Duplicate financial effects are not.&lt;/p&gt;

&lt;p&gt;An idempotency header is useful, but it is only the beginning of an idempotency design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the operation being protected
&lt;/h2&gt;

&lt;p&gt;An idempotency key needs a scope. The same string may be valid for one operation and invalid for another.&lt;/p&gt;

&lt;p&gt;A practical internal record can include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;owner_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;request_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pair &lt;code&gt;(operation, owner_key)&lt;/code&gt; identifies the business attempt being protected. Examples might include creating one external account for a user and currency, submitting one payout request, or provisioning one wallet on a network.&lt;/p&gt;

&lt;p&gt;The external idempotency key is then stable for retries of that same attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the key to the request body
&lt;/h2&gt;

&lt;p&gt;Reusing an idempotency key with a different payload is not a harmless retry.&lt;/p&gt;

&lt;p&gt;Imagine the first request attempts to send 100 units and the second attempts to send 1,000 using the same key. Returning the result of the first request would surprise the caller. Sending the second request with the same external key may be rejected by the provider. Executing both defeats idempotency entirely.&lt;/p&gt;

&lt;p&gt;Canonicalize and hash the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&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="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a retry arrives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same operation, owner, and hash: reuse the existing attempt;&lt;/li&gt;
&lt;li&gt;same operation and owner but different hash while pending: reject the conflict;&lt;/li&gt;
&lt;li&gt;completed attempt: return the stored result or begin a clearly defined new operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lock creation of the idempotency record
&lt;/h2&gt;

&lt;p&gt;Two workers can attempt to create the same record simultaneously. The application therefore needs both a database uniqueness constraint and transactional locking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IdempotencyRecord&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;owner_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;owner_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;request_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="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 database constraint is the final arbiter. Code-level checks improve behavior and error messages, but they do not replace uniqueness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist the resulting external resource
&lt;/h2&gt;

&lt;p&gt;An idempotency record is more useful when it stores the resource created by the operation.&lt;/p&gt;

&lt;p&gt;Suppose the external request succeeds but the API response is lost. On retry, the application should be able to determine whether an external resource was already linked locally rather than blindly creating another one.&lt;/p&gt;

&lt;p&gt;The completed record can retain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the local resource identifier;&lt;/li&gt;
&lt;li&gt;the external resource identifier;&lt;/li&gt;
&lt;li&gt;a sanitized response summary; and&lt;/li&gt;
&lt;li&gt;completion time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows safe replay and operational investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect the ledger independently
&lt;/h2&gt;

&lt;p&gt;Provider request idempotency does not automatically make the local financial effect idempotent.&lt;/p&gt;

&lt;p&gt;The external transfer may be created once while its success webhook is delivered multiple times. Therefore, use separate idempotency boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Command idempotency&lt;/strong&gt; prevents duplicate provider operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event idempotency&lt;/strong&gt; prevents processing the same provider event repeatedly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ledger idempotency&lt;/strong&gt; prevents applying the same credit or debit twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification idempotency&lt;/strong&gt; prevents duplicate customer messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These keys may derive from the same business reference, but they protect different effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what completion means
&lt;/h2&gt;

&lt;p&gt;An operation is not always complete when the provider accepts it.&lt;/p&gt;

&lt;p&gt;For asynchronous payments, useful stages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request safely submitted;&lt;/li&gt;
&lt;li&gt;external resource created;&lt;/li&gt;
&lt;li&gt;financial operation processing;&lt;/li&gt;
&lt;li&gt;terminal result received; and&lt;/li&gt;
&lt;li&gt;local ledger effect applied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idempotency record should reflect the boundary it protects. Marking everything completed immediately after an HTTP response can make later recovery difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not use random keys without durable storage
&lt;/h2&gt;

&lt;p&gt;Generating a new UUID for every retry provides uniqueness, not idempotency. The caller must reuse the same key for the same logical operation, or the server must derive and persist a stable association.&lt;/p&gt;

&lt;p&gt;The key’s lifecycle matters more than its randomness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;Idempotency is a business guarantee that repeated execution produces one intended effect. The HTTP header is simply one mechanism for carrying that intent across a network boundary.&lt;/p&gt;

&lt;p&gt;A reliable implementation defines the operation’s scope, binds it to a canonical request, serializes competing attempts, stores the result, and independently protects downstream financial effects.&lt;/p&gt;

&lt;p&gt;The resulting behavior is deterministic even when the network is not. A timeout can be retried without creating a new business operation, a conflicting payload is rejected, and downstream effects remain independently protected. This is a stronger guarantee than assuming retries will be rare.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Designing Explicit State Machines for Money Movement</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:14:57 +0000</pubDate>
      <link>https://dev.to/parksontano/designing-explicit-state-machines-for-money-movement-44b9</link>
      <guid>https://dev.to/parksontano/designing-explicit-state-machines-for-money-movement-44b9</guid>
      <description>&lt;p&gt;Turning scattered status updates into enforceable payment lifecycle rules.&lt;/p&gt;

&lt;p&gt;Payment systems often begin with a status column and a few convenient values: pending, successful, and failed.&lt;/p&gt;

&lt;p&gt;As the product grows, more states appear. A customer has authorized the payment, but the provider has not completed it. An agent has accepted an order but has not paid the recipient. Funds are reserved while an external transfer is processing. A transaction expires, but the customer can still dispute it.&lt;/p&gt;

&lt;p&gt;If every endpoint updates the status independently, the status field stops representing a lifecycle. It becomes a collection of guesses.&lt;/p&gt;

&lt;p&gt;An explicit state machine makes the permitted paths visible and enforceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status should describe business progress
&lt;/h2&gt;

&lt;p&gt;A useful payment state describes what the platform knows and which actions remain valid.&lt;/p&gt;

&lt;p&gt;A simplified lifecycle might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending -&amp;gt; processing -&amp;gt; completed
   |           |
   |           +------&amp;gt; failed
   +------------------&amp;gt; cancelled
   +------------------&amp;gt; expired

processing or expired -&amp;gt; disputed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact states depend on the product. What matters is that transitions are intentional.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pending&lt;/strong&gt;: the transaction exists but no party has committed to fulfillment;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;processing&lt;/strong&gt;: an external provider or human operator is actively fulfilling it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completed&lt;/strong&gt;: the financial obligation has been satisfied;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;failed&lt;/strong&gt;: fulfillment ended unsuccessfully;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cancelled&lt;/strong&gt;: the transaction was intentionally stopped while cancellation was still valid;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;expired&lt;/strong&gt;: the allowed action window ended;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;disputed&lt;/strong&gt;: normal completion rules are suspended for investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Put transition rules in the domain layer
&lt;/h2&gt;

&lt;p&gt;Endpoints should request transitions rather than assign statuses directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disputed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disputed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispute_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps API endpoints, webhooks, background workers, and staff tools from inventing different transition rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminal states need strong protection
&lt;/h2&gt;

&lt;p&gt;Completed, cancelled, and failed transactions are usually terminal. A delayed event should not reopen them.&lt;/p&gt;

&lt;p&gt;Terminal protection should be checked while the transaction is locked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lock_transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&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;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TERMINAL_STATES&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;transfer&lt;/span&gt;

    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_provider_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the lock, two actors may both observe a non-terminal state and trigger conflicting side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side effects belong to transitions
&lt;/h2&gt;

&lt;p&gt;A status change is rarely the only effect. Completing a withdrawal may also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convert a hold into a debit;&lt;/li&gt;
&lt;li&gt;write a ledger entry;&lt;/li&gt;
&lt;li&gt;complete a linked transaction;&lt;/li&gt;
&lt;li&gt;record a receipt;&lt;/li&gt;
&lt;li&gt;notify the customer; and&lt;/li&gt;
&lt;li&gt;close an operator task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These effects should be coordinated by one transition service. If each caller independently updates related records, the system can end up with a completed transaction, an unreleased hold, and a still-open operational task.&lt;/p&gt;

&lt;p&gt;Where possible, financial and state changes should be atomic. Notifications and other external work can be queued after the database commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expiration is a state transition, not a UI timer
&lt;/h2&gt;

&lt;p&gt;A countdown displayed in an application is not authoritative. The backend must determine whether the deadline has passed and whether expiration is still legal.&lt;/p&gt;

&lt;p&gt;Only eligible states should expire. A transaction already processing with an external rail should not be expired merely because its original initiation deadline passed. Doing so creates a race between an expiration task and a late success event.&lt;/p&gt;

&lt;p&gt;Background expiration workers should lock each candidate and re-check its state before applying the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linked transactions require one completion authority
&lt;/h2&gt;

&lt;p&gt;Financial products sometimes represent one customer action through two linked records. For example, a withdrawal request may be fulfilled through a marketplace transfer.&lt;/p&gt;

&lt;p&gt;The dangerous design lets either record complete independently. That can duplicate notifications, ledger mutations, or payouts.&lt;/p&gt;

&lt;p&gt;Instead, define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which transaction owns the customer-facing lifecycle;&lt;/li&gt;
&lt;li&gt;which one represents fulfillment;&lt;/li&gt;
&lt;li&gt;which transition completes the linked record;&lt;/li&gt;
&lt;li&gt;how retries detect that completion already occurred; and&lt;/li&gt;
&lt;li&gt;how cancellation and disputes propagate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preserve timestamps for meaningful transitions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;updated_at&lt;/code&gt; is not enough. Store timestamps that answer operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when was the order accepted?&lt;/li&gt;
&lt;li&gt;when did the customer mark it paid?&lt;/li&gt;
&lt;li&gt;when did processing start?&lt;/li&gt;
&lt;li&gt;when was it completed?&lt;/li&gt;
&lt;li&gt;when did it expire?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These timestamps support support investigations, service-level monitoring, dispute handling, and future analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;A transaction status is a compressed representation of business truth. It should change only through named transitions that enforce prerequisites and coordinate their financial effects.&lt;/p&gt;

&lt;p&gt;When the state machine is explicit, asynchronous providers, human operators, background jobs, and staff tools can all participate without silently contradicting one another.&lt;/p&gt;

&lt;p&gt;This structure also makes failures easier to reason about. Instead of asking which endpoint last edited a status string, engineers can inspect a defined transition, its prerequisites, and the side effects that should have occurred with it. That clarity is valuable during development, operations, and reconciliation.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Webhooks Are Notifications, Not Your Source of Truth</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:08:47 +0000</pubDate>
      <link>https://dev.to/parksontano/webhooks-are-notifications-not-your-source-of-truth-1fi7</link>
      <guid>https://dev.to/parksontano/webhooks-are-notifications-not-your-source-of-truth-1fi7</guid>
      <description>&lt;p&gt;Building payment workflows that remain correct when events are duplicated, delayed, missing, or delivered out of order.&lt;/p&gt;

&lt;p&gt;Payment integrations often look simple in a sequence diagram:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a transaction.&lt;/li&gt;
&lt;li&gt;Send it to a payment provider.&lt;/li&gt;
&lt;li&gt;Receive a webhook.&lt;/li&gt;
&lt;li&gt;Mark the transaction successful.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The difficult part begins when production stops following the diagram.&lt;/p&gt;

&lt;p&gt;A webhook may arrive twice. A “processing” event may arrive after a “successful” event. The provider may accept an API request with HTTP 200 while the underlying payment remains pending. A status check may return a transport-level success code that has nothing to do with the financial result. In some cases, the final webhook may never arrive.&lt;/p&gt;

&lt;p&gt;When money is involved, a webhook cannot be treated as a command to change a balance. It is evidence about an external event. The local transaction record must decide whether that evidence permits a state transition and whether the related financial effect has already happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the transaction before trusting asynchronous events
&lt;/h2&gt;

&lt;p&gt;Every external payment should map to an internal transaction created before the asynchronous lifecycle begins. That record should contain enough stable identifiers to match later provider events without relying on mutable fields such as a description or customer-entered name.&lt;/p&gt;

&lt;p&gt;A simplified record might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;provider_reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;credited_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local reference identifies the business operation. The provider reference identifies the external attempt. They serve different purposes and should not be silently substituted for each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate transport success from financial success
&lt;/h2&gt;

&lt;p&gt;An HTTP success response can mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider received the request;&lt;/li&gt;
&lt;li&gt;the request passed initial validation;&lt;/li&gt;
&lt;li&gt;the operation was queued;&lt;/li&gt;
&lt;li&gt;the provider returned a status payload successfully; or&lt;/li&gt;
&lt;li&gt;the financial transaction completed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the last meaning should trigger a final credit or completion.&lt;/p&gt;

&lt;p&gt;I prefer translating provider responses into a small internal vocabulary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queued&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;settled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;FAILURE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancelled&lt;/span&gt;&lt;span class="sh"&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 real mapping varies by provider, but the rule remains constant: unknown and intermediate states should not be promoted to success.&lt;/p&gt;

&lt;p&gt;This is especially important when a provider exposes both a general response code and a transaction status. A successful status-check request does not prove that the payment succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make every financial effect idempotent
&lt;/h2&gt;

&lt;p&gt;Webhook delivery is normally at least once. Therefore, the same success event must be safe to process repeatedly.&lt;/p&gt;

&lt;p&gt;Checking only the transaction status is helpful but incomplete. Two workers can read the same pending status before either one commits. Both may then attempt the credit.&lt;/p&gt;

&lt;p&gt;A stronger design protects the financial effect with its own deterministic identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lock_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;ledger_entry_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deposit:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TERMINAL_STATES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignored&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nf"&gt;create_ledger_credit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deposit:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The row lock prevents concurrent transitions. The unique ledger key prevents duplicate financial effects even if application logic is retried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect terminal states from older events
&lt;/h2&gt;

&lt;p&gt;External systems do not always deliver events in chronological order. Once a transaction reaches a trusted terminal state, older events must not move it backward.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ALLOWED_TRANSITIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&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;This prevents a delayed “processing” notification from reopening a completed transaction. It also stops a late failure from automatically reversing a payment that has already been independently confirmed, unless the product explicitly supports a separate reversal workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve raw evidence without exposing it to customers
&lt;/h2&gt;

&lt;p&gt;Webhook payloads are useful for investigation, reconciliation, and provider support. Retain them with appropriate access controls, but don't make them the customer-facing transaction model.&lt;/p&gt;

&lt;p&gt;Store both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a normalized internal status and safe customer message;&lt;/li&gt;
&lt;li&gt;the original provider event for staff diagnostics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation prevents technical provider errors, internal account balances, or implementation details from leaking into the customer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an active recovery path
&lt;/h2&gt;

&lt;p&gt;Even a perfect webhook handler cannot process an event that never arrives. Transactions that remain in a non-terminal state need background reconciliation.&lt;/p&gt;

&lt;p&gt;A recovery worker can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find processing transactions older than a safe threshold.&lt;/li&gt;
&lt;li&gt;Query the provider using the recorded provider reference.&lt;/li&gt;
&lt;li&gt;Normalize the returned state.&lt;/li&gt;
&lt;li&gt;Apply the same guarded transition used by webhook processing.&lt;/li&gt;
&lt;li&gt;Retry with limits when the provider remains inconclusive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The webhook and reconciliation paths should converge on the same domain function. Otherwise, each path develops different rules for crediting, failure, and refunds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;The transaction record is authoritative for what the platform has applied. The provider remains authoritative for what happened on its rail. Webhooks and status checks are two ways of collecting provider evidence; neither should bypass local invariants.&lt;/p&gt;

&lt;p&gt;That distinction turns webhook handling from a collection of callbacks into a reliable payment lifecycle.&lt;/p&gt;

&lt;p&gt;The practical outcome of this architecture is predictable behavior under uncertainty. Duplicate delivery becomes harmless, intermediate states remain intermediate, and a missing callback becomes a reconciliation problem instead of an indefinitely stuck payment. Those are properties that can be verified through invariants and tests without relying on an unsupported performance claim.&lt;/p&gt;

</description>
      <category>programming</category>
    </item>
    <item>
      <title>Making Wallet Mutations Safe Under Concurrency</title>
      <dc:creator>Daniel Parkson Tano</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:08:12 +0000</pubDate>
      <link>https://dev.to/parksontano/making-wallet-mutations-safe-under-concurrency-3g55</link>
      <guid>https://dev.to/parksontano/making-wallet-mutations-safe-under-concurrency-3g55</guid>
      <description>&lt;p&gt;Why checking a balance and then updating it is not enough in a financial system.&lt;/p&gt;

&lt;p&gt;A wallet can be represented by one database column. A safe wallet cannot.&lt;/p&gt;

&lt;p&gt;Consider a user with a balance of 100. Two withdrawal requests for 80 arrive almost simultaneously. Each request reads the balance before the other commits. Both see 100, both pass validation, and both subtract 80.&lt;/p&gt;

&lt;p&gt;The application performed the expected check twice, yet it still spent the same money twice.&lt;/p&gt;

&lt;p&gt;This is the central concurrency problem in wallet systems: validation and mutation must be one indivisible operation against a locked financial record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the invariant inside the transaction
&lt;/h2&gt;

&lt;p&gt;The basic invariant is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A debit may commit only when the locked balance is greater than or equal to the debit amount.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A sanitized implementation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;wallet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;currency&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;raise&lt;/span&gt; &lt;span class="nc"&gt;InsufficientFunds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;before&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;balance&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;balance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
    &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;LedgerEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debit&lt;/span&gt;&lt;span class="sh"&gt;"&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;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;balance_before&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;balance_after&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;balance&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 row lock serializes competing mutations for the same wallet. Validation uses the balance that is current while the lock is held, rather than a stale value read earlier in the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the mutation idempotent
&lt;/h2&gt;

&lt;p&gt;Concurrency safety prevents two different operations from overspending the wallet. Idempotency prevents one operation from being applied twice.&lt;/p&gt;

&lt;p&gt;A deterministic mutation key can be derived from stable business data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transaction_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;mutation_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ledger should enforce uniqueness on that key. An application-level existence check is useful for a clear response, but the unique database constraint is the final protection against racing workers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;database_transaction&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;LedgerEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mutation_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;already applied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;wallet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lock_wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;apply_mutation&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="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;create_ledger_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mutation_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If two workers pass the existence check at the same time, the uniqueness constraint still prevents both ledger rows from being committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep balance history with the mutation
&lt;/h2&gt;

&lt;p&gt;A wallet table answers “what is the balance now?” It does not explain how the balance arrived there.&lt;/p&gt;

&lt;p&gt;Every mutation should record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the business reference;&lt;/li&gt;
&lt;li&gt;direction and transaction type;&lt;/li&gt;
&lt;li&gt;currency and amount;&lt;/li&gt;
&lt;li&gt;balance before;&lt;/li&gt;
&lt;li&gt;balance after;&lt;/li&gt;
&lt;li&gt;idempotency key;&lt;/li&gt;
&lt;li&gt;timestamp; and&lt;/li&gt;
&lt;li&gt;relevant sanitized metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates an audit trail that supports customer support, reconciliation, incident investigation, and financial reporting.&lt;/p&gt;

&lt;p&gt;The balance and ledger entry should commit in the same database transaction. A balance change without an audit entry is difficult to explain. An audit entry without the corresponding balance change is equally dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat each currency as a separate wallet
&lt;/h2&gt;

&lt;p&gt;Multi-currency systems become easier to reason about when each user-currency pair has one authoritative wallet record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;UniqueConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conversions should be recorded as linked financial movements rather than an unexplained overwrite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;debit the source wallet;&lt;/li&gt;
&lt;li&gt;record the source amount;&lt;/li&gt;
&lt;li&gt;record the applied exchange rate;&lt;/li&gt;
&lt;li&gt;credit the destination wallet;&lt;/li&gt;
&lt;li&gt;record the destination amount;&lt;/li&gt;
&lt;li&gt;preserve one conversion reference across both effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conversion rate is part of the transaction’s history. Looking up today’s rate later cannot reconstruct what the customer received at execution time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model holds separately from completed debits
&lt;/h2&gt;

&lt;p&gt;Some operations need to reserve money before completion. A withdrawal may be awaiting an external provider or manual review. Immediately treating the reservation as a final debit makes failure recovery harder.&lt;/p&gt;

&lt;p&gt;A clearer model distinguishes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total or actual balance;&lt;/li&gt;
&lt;li&gt;held balance;&lt;/li&gt;
&lt;li&gt;available balance; and&lt;/li&gt;
&lt;li&gt;completed ledger balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Available funds can then be expressed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available = balance - active_holds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On success, the hold becomes a committed debit. On failure, the hold is released. Both transitions should be idempotent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not hide negative states with arithmetic tricks
&lt;/h2&gt;

&lt;p&gt;Operational adjustments, chargebacks, and debt recovery introduce another challenge. A system may need to represent an amount owed without presenting an impossible or misleading wallet state.&lt;/p&gt;

&lt;p&gt;The safest approach is to model debt, holds, and display policy explicitly. Mixing them into a single balance field makes future credits, support investigations, and reconciliation much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;A financial mutation is not an assignment such as &lt;code&gt;balance = balance - amount&lt;/code&gt;. It is a domain operation with concurrency rules, an immutable identity, an audit record, and explicit failure behavior.&lt;/p&gt;

&lt;p&gt;The database is not merely storage in this design. Its transaction boundaries, row locks, and constraints are part of the payment system’s correctness model.&lt;/p&gt;

&lt;p&gt;The value of this design is structural: concurrent requests cannot safely bypass the locked balance check, and a repeated business operation cannot silently create a second ledger mutation. Correctness comes from enforceable database guarantees rather than assumptions about how often a request will be retried.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
