When you build a feature for a typical app—like a social feed or a profile page—a software bug is usually just an annoyance. A user might have to refresh their page, or a button might not click.
In a payment system, the stakes are entirely different.
A single bug can withdraw real money from a customer’s bank account twice, lose track of a transaction entirely, or accidentally deposit funds you don’t have. In this world, getting it right is infinitely more important than getting it fast.
This guide is a complete, beginner-friendly blueprint for designing a payment back-end. We will demystify the technical jargon and explain exactly how to build a system where money never goes missing, even when the internet cuts out mid-transaction.
1. The Three Ways to Accept Payments
If you want your app to take payments, you don’t have to build a bank. Instead, you integrate with a Payment Service Provider (PSP)—companies like Stripe, PayPal, or Lemon Squeezy that act as translators between the modern internet and the legacy banking system.
Depending on your business needs, you can choose one of three integration routes:
| Route | How It Works | The Good | The Bad |
1. Hosted Checkout (No-Code) | You put a button on your website that redirects users to a pre-built page hosted by Stripe or PayPal. | Extremely fast to set up. You can go live in minutes. | You have zero control over the design. It is very hard to handle complex things like shopping carts or customized subscription plans.
2. Your Own Processor (The DIY Route) | You bypass the middlemen entirely, apply for banking licenses, and connect directly to Visa and Mastercard. | The lowest transaction fees possible. | It takes years of legal work, strict security audits, and millions of dollars. Only giants like Amazon operate at this scale.
3. API Integration (The Sweet Spot) | You use a PSP's hidden infrastructure but build your own custom checkout screen. This is what we are designing. | You get complete control over the user experience without any of the legal or banking headaches. | It requires meticulous back-end design to ensure you don't accidentally double-charge people. |
2. The Secret Journey of a Credit Card
To build a reliable system, you first need to understand what happens behind the scenes when a customer enters their card number and clicks "Pay."
Every transaction is split into two distinct steps to prevent fraud and errors.
Step 1: Authorization (Holding the Money)
When a customer submits their card details, your app securely hands them to the PSP (e.g., Stripe). The PSP asks the customer’s bank:
"Does this card exist, and do they have $50?"
If the bank says yes, they put a temporary hold on that $50. The money hasn't left the customer's account yet, but they can't spend it elsewhere. It is "reserved." This is called an Authorized payment.
Step 2: Capture (Taking the Money)
Once you confirm everything is in order, your backend tells the PSP to actually claim that reserved money. This is called Capturing the payment.
- Immediate Capture: For digital items or subscriptions, you authorize and capture the money immediately.
- Delayed Capture: For physical items (like ordering on Amazon), the money is only authorized (held) when you order, and only captured (taken) when the item actually ships.
3. The Blueprint: How the System is Structured
To keep the system highly reliable, we divide our backend architecture into two distinct pathways: the Synchronous (Instant) Path and the Asynchronous (Background) Path.
The Synchronous Path (What the User Sees)
This path is optimized for speed. When a user clicks "Pay," we want to give them an instant response without keeping them waiting on slow banking networks.
- The Checkout Page: The user types their card info into secure forms provided by your PSP. Your servers never actually touch or see the raw credit card number (which keeps you safe from massive security liabilities).
- The Payment API: Your backend receives a "request to pay." Before doing anything, it assigns a unique tracking record to this transaction.
- Creating an "Intent": Your backend tells the PSP, "We intend to charge this customer $50." The PSP returns a secret code. Your frontend uses this code to securely complete the payment with the customer's bank.
The Asynchronous Path (The Heavy Lifting in the Background)
Because banks can be slow and network connections can drop, we do the final confirmation of the payment in the background using Webhooks.
A Webhook is simply the PSP calling your backend's phone number to say:
"Hey, just letting you know that $50 charge went through successfully."
- The Webhook Receiver: A dedicated listener that waits for the PSP's confirmation. It must do its job in under a second: verify the message is genuinely from the PSP, save the raw message to a database, say "Got it!", and hang up.
- The Message Queue: A digital assembly line. The Webhook Receiver drops the payment confirmation onto this conveyor belt so it doesn't get lost if your system temporarily crashes.
- Background Workers: Virtual employees that grab messages from the conveyor belt one by one. They update your database to mark the user as "Paid," unlock their premium features, and record the transaction in your company ledger.
4. Bulletproofing Your System: The Core Challenges
The hardest part of building a payment system is handling the messy reality of the internet. Servers crash, users double-click buttons, and networks drop. Here is how we prevent disaster.
Problem A: The Double-Click (Preventing Double Charges)
If a user has a slow connection, they might click the "Submit Payment" button three times. Without protection, your server might send three separate charge requests to the bank.
The Solution: Idempotency Keys
An Idempotency Key is a unique "ticket number" generated by the user's browser the moment they open the checkout screen.
No matter how many times the user clicks submit, or how many times your server retries a failing connection, they always send that exact same ticket number. Your bac-kend checks its database: "Have I seen ticket ID-999 before?"
- If no, it processes the payment.
- If yes, and it's already paid, it simply hands back the successful receipt without charging the card again.
- If yes, but it crashed halfway through, it picks up exactly where it left off instead of starting over.
Problem B: Crashing in the Middle of a Transaction
Imagine your server successfully tells Stripe to charge the customer $50. Stripe does it. But right before Stripe can send back the confirmation, your server loses power. Stripe thinks the payment is done, but your database still says "Pending." The customer was charged, but they didn't get their digital item!
The Solution: Two-Phase Status Validation & Ledgers
To solve this, your database must strictly enforce a Finite State Machine (FSM). This is a rulebook that says a payment can only move from state to state in a logical, step-by-step order (e.g., a payment cannot go straight from Created to Completed without passing through Pending and Authorized).
We also use Double-Entry Bookkeeping (Ledgering). Just like a real accountant, our system never "edits" or "deletes" past transaction records to fix a mistake. If we accidentally recorded a wrong charge, we do not erase it. Instead, we write a completely new, balancing entry (a refund entry) to correct the math. This creates an unalterable paper trail for every single penny.
Problem C: Consistency over Availability
In most web apps, if a database is temporarily out of sync and shows a user an old profile picture for 5 seconds, nobody cares.
In payments, if a database is out of sync and shows a user they have $0 instead of $100, they will panic.
Because of this, payment back-ends prioritize Consistency over Availability. If our databases are struggling to synchronize, we would rather show a clean, polite "Service temporarily unavailable, please try again in a moment" error message than display incorrect account balances or let a transaction slip through unrecorded.

Top comments (0)