May 19, 2026
Payment Backend 101 — From a Beginner Perspective
In most software systems, a bug costs you uptime. A wrong calculation might show a weird number on a dashboard, trigger an alert, and it…

By Aryan Srivastava
5 min read
In most software systems, a bug costs you uptime. A wrong calculation might show a weird number on a dashboard, trigger an alert, and it will get fixed in the next deploy.
In fintech, a bug costs real money.
Every decision in a payment system carries weight. Rounding errors, race conditions, missing retries — these aren't edge cases to handle someday. They are the system. Get them wrong and you lose user's money.
In fintech system failures are expected from day one and systems are designed to handle all kinds of failures so no money is lost. Reliability is everything here.
Chapter 1: Never Use Floating-Point Numbers for Money
If you have worked with numbers in code, using a float or double feels completely natural. It's what most languages default to, it's fast, and it works fine in most contexts.
But in fintech systems, this habit can silently introduce errors that compound into real money loss. Let's understand how -
Floating-point numbers store values in binary. The number 0.1 cannot be stored precisely in binary — it becomes an infinitely repeating fraction, similar to how 1/3 can't be written precisely in decimal.
A simple example:
0.1 + 0.2
You'd expect 0.3. What you actually get:
0.30000000000000004
In isolation, this looks harmless. A tiny, almost invisible error.
But real payment systems process millions or billions of transactions every day. Even a drift of ₹0.01, repeated a million times becomes ₹10,000 in unexplained discrepancy.
The fix
Never use unguarded or undefined floating-point types for money.
There are two common approaches:
1. Store amounts as integers in the smallest unit
2. Use fixed-precision decimal types
For most systems, integer storage in the smallest unit is simpler, because if you use fixed -precision, then that fixed precision value becomes a variable which needs to be managed across different systems.
To wrap up, if you simulate adding ₹0.01 one million times:
- Using float → you will see drift
- Using integer → exact result
- Using precise decimal → exact result
Chapter 2: The Append-Only Ledger
Consider a payment of ₹500 that was processed ten days ago and later updated today. If the original state is overwritten, the historical record is effectively lost. During debugging, developers are forced to reconstruct events based on assumptions rather than reliable data.
In fintech systems, this is not merely a minor bug — it reflects a fundamental flaw in system design. Financial systems must preserve accurate historical state at all times, because in domains involving real money, data integrity and traceability are non-negotiable.
Currency is a real sensitive data and we should track every change of it, a single number in row is definitely not the most secure way of doing it.
So instead of updating existing records, every financial event is written as a new entry. Each one becomes a permanent record in the ledger.
Most fintech systems follow the principle of double-entry accounting. Every transaction creates two ledger entries:
- A debit from one account
- A corresponding credit to another account
Money never simply "appears" or "disappears" inside the system — it always moves from one ledger account to another. This guarantees that the books remain balanced and makes inconsistencies significantly easier to detect.
For example:
- User Wallet → Debit ₹500
- Merchant Settlement Account → Credit ₹500
Every movement has an equal and opposite entry.
What this gives you
Audit trail. Every event is preserved exactly as it is occurred. Teams can trace every rupee through the system at any point in time and the current state of any account is derived from the complete history of events, not from a single number.
Reconciliation. It is the process of comparing the company's internal transaction history with actual bank records (for example ICICI or SBI bank statements) to ensure that every payment, refund, and balance is consistent and accounted for.
Without a complete and immutable ledger history, reliable reconciliation and event sourcing becomes impossible, making it extremely difficult to detect discrepancies, investigate failures, or maintain financial accuracy.
Chapter 3: Idempotency: Handling Retries Safely
I ran out of memes ideas ;)
Network requests fail. A request times out, the client retries, and now the same payment has been sent twice. Without any protection, both go through and the client will be charged twice if not handled correctly.
Idempotency is how you prevent this. Idempotency means the same action can be performed multiple times and the results remains the same. For example fetching something from the db is idempotent, fetch it once or 10 times the result remains the same but adding a new entry in the db is not.
How it works
The client generates a unique key before sending the request and includes it in the header, typically as Idempotency-Key. This key travels with the request every time it is sent, including retries.
On the server side, the payment gateway checks this key against a cache typically Redis before doing anything. If the key has been seen before, it returns the original result immediately without processing the request again. If it hasn't, it processes the payment and stores the key along with the result.
This means the client can retry as aggressively as it needs to. The gateway ensures the operation happens exactly once.
Inside the database
Idempotency does not stop at the payment gateway. When ledger entries are created, the idempotency key is persisted alongside the transaction itself. This becomes critical because the creation of new operations may still encounter concurrency issues, especially in scenarios where traditional row-level locking cannot fully prevent duplicate writes.
This stored idempotency key acts as a second layer of protection. Even if a duplicate request bypasses the cache or retry protection at the gateway layer, the database can still detect that the operation has already been processed and reject the duplicate entry.
Two layers, one guarantee: regardless of how many times the same request arrives, the transaction is recorded exactly once.
Chapter 4: Where Strings Come In
You might notice that many payment APIs represent amounts as strings:
{ "amount": "100.50" }
This often causes confusion. Why a string? Why not just send a number?
The answer is about what happens at system boundaries. When data is serialized into JSON and sent over a network, numbers can lose precision depending on the language or runtime parsing them. JavaScript, for example, cannot safely represent all large integers or precise decimals natively — a float slips in at the parsing stage before your code even sees the value.
A string carries no such risk. "100.50" arrives exactly as "100.50", in every language, on every platform, every time.
This is purely a transport concern. Strings are used at the boundary in the request and response — not internally. The moment the value enters the backend, it is converted into an integer in the smallest unit, paise or cents, and that is what every subsequent operation works with.
A Simple Mental Model
A typical flow in a payment system looks like this:
- API receives amount as a string
- Backend converts it into an integer (smallest unit)
- Two ledger entries are created (debit and credit)
- Entries are appended to the ledger
- Idempotency is checked to avoid duplicates
- Balance is computed from the ledger