<?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: KingCodex</title>
    <description>The latest articles on DEV Community by KingCodex (@king_codex).</description>
    <link>https://dev.to/king_codex</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%2F4022008%2F9d1d454b-7489-4bf6-bfa3-ea75a80aa303.jpg</url>
      <title>DEV Community: KingCodex</title>
      <link>https://dev.to/king_codex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/king_codex"/>
    <language>en</language>
    <item>
      <title>Two Things Fintech Backends Get Wrong</title>
      <dc:creator>KingCodex</dc:creator>
      <pubDate>Sat, 11 Jul 2026 16:41:07 +0000</pubDate>
      <link>https://dev.to/king_codex/two-things-fintech-backends-get-wrong-2e63</link>
      <guid>https://dev.to/king_codex/two-things-fintech-backends-get-wrong-2e63</guid>
      <description>&lt;p&gt;Most backend tutorials stop at "hash the password, you're secure now." That's good advice for authentication. It's not much use for the rest of a payment system, where you're dealing with data you actually need back and events that don't politely arrive exactly once.&lt;/p&gt;

&lt;p&gt;I work on payment infrastructure. Two problems come up constantly here that I almost never see discussed outside of "we had an incident" postmortems: encrypting sensitive fields correctly and making webhook handlers actually idempotent instead of just idempotent-looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt;: the part hashing can't help you with&lt;/p&gt;

&lt;p&gt;Password hashing works because it's one-way. You never need the plaintext back. You just need to compare a new hash against the stored one.&lt;/p&gt;

&lt;p&gt;Bank account numbers don't work like that. Neither do BVNs or NINs. You need the original value later, which means encryption has to be reversible. Once that's true, the security story depends far more on how you manage your encryption keys than on which algorithm you picked.&lt;/p&gt;

&lt;p&gt;For a while I didn't think much about which AES mode to use. AES is AES, right?&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;AES is just the block cipher. The mode of operation determines whether your ciphertext is tamper-evident, and that matters a lot.&lt;/p&gt;

&lt;p&gt;CBC mode encrypts data just fine, but it doesn't authenticate anything. If someone flips bits in the ciphertext, CBC will happily decrypt it into corrupted plaintext and hand it back to your application without complaint.&lt;/p&gt;

&lt;p&gt;It gets worse. If your decryption path leaks any information about whether padding was valid, whether through different error messages, timing differences, or different HTTP status codes, an attacker can use that signal to recover plaintext one byte at a time without ever knowing your key.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. It's the same class of vulnerability behind the ASP.NET ViewState attack in 2010 and attacks like Lucky Thirteen and POODLE against TLS.&lt;/p&gt;

&lt;p&gt;Using CBC without a separate HMAC in an encrypt-then-MAC design is essentially waiting for someone to make a mistake elsewhere in the system.&lt;/p&gt;

&lt;p&gt;GCM avoids this by providing authenticated encryption.&lt;/p&gt;

&lt;p&gt;Instead of producing only ciphertext, it also generates an authentication tag. During decryption, the tag is verified before any plaintext is released. If verification fails, decryption stops immediately. No plaintext is exposed and no padding information leaks, so there is no oracle for an attacker to exploit.&lt;/p&gt;

&lt;p&gt;For anything sensitive that needs to be decrypted later, such as account numbers, BVNs, or NINs, AES-256-GCM is a solid default.&lt;/p&gt;

&lt;p&gt;In a NestJS + TypeORM application, I prefer hiding the encryption behind a transformer or subscriber so the rest of the application works with plaintext objects in memory without worrying about encryption details.&lt;/p&gt;

&lt;p&gt;The difficult part isn't calling the encryption function. It's key management.&lt;/p&gt;

&lt;p&gt;You need versioned encryption keys so records encrypted years ago with key v1 can still be decrypted after you've rotated to v3.&lt;/p&gt;

&lt;p&gt;One rule that's worth remembering:&lt;/p&gt;

&lt;p&gt;Never reuse the same key and nonce combination with GCM.&lt;/p&gt;

&lt;p&gt;Do it once and you don't just weaken the encryption. You can completely compromise its security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;: the database constraint isn't enough&lt;/p&gt;

&lt;p&gt;Payment providers retry webhooks.&lt;/p&gt;

&lt;p&gt;That's not a bug. It's part of the contract.&lt;/p&gt;

&lt;p&gt;If your endpoint times out or returns an error, providers assume they don't know whether you processed the event successfully, so they try again. If your handler isn't safe to execute multiple times, one temporary network failure can become a duplicate credit, duplicate notification, or duplicate payout.&lt;/p&gt;

&lt;p&gt;The obvious solution is adding a unique constraint on the event ID.&lt;/p&gt;

&lt;p&gt;That's necessary.&lt;/p&gt;

&lt;p&gt;It isn't sufficient.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Check whether the event already exists.&lt;/li&gt;
&lt;li&gt;If it doesn't, process the webhook.&lt;/li&gt;
&lt;li&gt;Record the event as processed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problem is that two copies of the same webhook can arrive almost simultaneously.&lt;/p&gt;

&lt;p&gt;Both requests check the database.&lt;/p&gt;

&lt;p&gt;Both see that the event doesn't exist.&lt;/p&gt;

&lt;p&gt;Both perform every side effect, including sending emails or calling external APIs.&lt;/p&gt;

&lt;p&gt;Only afterward does one insert fail because of the unique constraint.&lt;/p&gt;

&lt;p&gt;The duplicate database row is prevented.&lt;/p&gt;

&lt;p&gt;The duplicate email already went out.&lt;/p&gt;

&lt;p&gt;The fix is making the claim atomic before any side effects happen.&lt;/p&gt;

&lt;p&gt;Instead of checking first, attempt the insert immediately:&lt;/p&gt;

&lt;p&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/p&gt;

&lt;p&gt;If the insert succeeds, you won the race and can continue processing.&lt;/p&gt;

&lt;p&gt;If it affects zero rows, another worker already owns the event. Return success and stop.&lt;/p&gt;

&lt;p&gt;Redis follows exactly the same principle.&lt;/p&gt;

&lt;p&gt;Don't do:&lt;/p&gt;

&lt;p&gt;GET&lt;br&gt;
SET&lt;/p&gt;

&lt;p&gt;Do:&lt;/p&gt;

&lt;p&gt;SET webhook:{event_id} 1 NX EX 86400&lt;/p&gt;

&lt;p&gt;The "NX" option makes the claim atomic. "GET" followed by "SET" has the same race condition as checking the database before inserting.&lt;/p&gt;

&lt;p&gt;There are two more lessons that only really stick after production incidents.&lt;/p&gt;

&lt;p&gt;First, don't keep your claim and network calls inside one long-running database transaction.&lt;/p&gt;

&lt;p&gt;If a third-party API takes ten seconds to respond, you've just held database locks open for ten seconds. Under load, those blocked transactions pile up surprisingly quickly.&lt;/p&gt;

&lt;p&gt;Second, claiming the event doesn't mean the work is finished.&lt;/p&gt;

&lt;p&gt;If your process crashes after claiming ownership but before completing the work, every retry will see the existing claim and skip the event entirely.&lt;/p&gt;

&lt;p&gt;A more reliable approach stores a status such as "pending", "processing", and "done" instead of a simple boolean.&lt;/p&gt;

&lt;p&gt;Another common solution is the outbox pattern, where the claim and the outbound work are committed in the same transaction, then a separate worker performs the external side effects.&lt;/p&gt;

&lt;p&gt;Why write this down?&lt;/p&gt;

&lt;p&gt;None of this is especially complicated.&lt;/p&gt;

&lt;p&gt;That's exactly why it's easy to ignore.&lt;/p&gt;

&lt;p&gt;The happy path works.&lt;/p&gt;

&lt;p&gt;Your local tests pass.&lt;/p&gt;

&lt;p&gt;Your demo behaves perfectly.&lt;/p&gt;

&lt;p&gt;The problems only appear once you're dealing with real retries, real concurrency, and real production traffic.&lt;/p&gt;

&lt;p&gt;By the time they appear, they're usually customer support tickets instead of code review comments.&lt;/p&gt;

&lt;p&gt;If you're building anything that handles money or personally identifiable information, both of these should be defaults from day one rather than security improvements you hope to add later.&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>security</category>
      <category>nestjs</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
