Definition
- Idempotency key : a client-generated unique id per payment so retries return the same result instead of double-charging.
- Double-spend prevention : atomic conditional update or optimistic locking ensuring the same balance is not committed twice.
- Two-phase (authorize-capture) : authorize reserves funds; capture settles them. Uncaptured auths can be voided.
- Double-entry ledger : every transaction writes a debit and a matching credit; the signed sum is always zero.
- PCI DSS : the system never stores raw card numbers; it tokenizes through a compliant vault.
When Rajesh pays Priya for rent through National Bank of India, the bank first checks and reserves the money (authorize), then actually moves it (capture). If Rajesh's phone retries because the network blinked, the bank recognizes the same payment and does not charge twice. Every rupee movement is written in a permanent ledger with two entries : money out and money in.
Understanding the Concepts
How does idempotency work?
The client generates a unique key per payment. The server stores results by key, so replays return the same outcome. If Rajesh's phone retries because the network timed out, the server finds the previous result for that key and returns it without charging again.
package com.bank.payment;
public class IdempotentPaymentService {
public PaymentResult pay(String idemKey, PaymentRequest req) {
PaymentResult prior = idempotencyStore.get(idemKey);
if (prior != null) return prior; // safe retry
PaymentResult result = execute(req);
idempotencyStore.put(idemKey, result);
return result;
}
}
How do you prevent double-spend?
Atomic conditional update or optimistic locking ensures the balance check and debit happen as one step. If two concurrent requests try to spend the last 500 rupees, only one succeeds because the version check fails for the second.
package com.bank.payment;
import java.math.BigDecimal;
public class DoubleSpendPrevention {
public boolean debit(String account, BigDecimal amount, long expectedVersion) {
int rows = jdbc.update(
"UPDATE accounts SET balance = balance - ?, version = version + 1 " +
"WHERE id = ? AND version = ? AND balance >= ?",
amount, account, expectedVersion, amount);
return rows == 1; // 0 = concurrent update, retry
}
}
How does authorize-then-capture work?
Authorize reserves funds by reducing available balance without actually moving money. Capture settles the reserved amount. This matches card network flows and allows clean void of uncaptured auths. At National Bank of India, when Rajesh books a hotel, the bank authorizes the amount immediately but captures only at checkout.
package com.bank.payment;
import java.math.BigDecimal;
public class TwoPhasePayment {
public String authorize(String account, BigDecimal amount) {
accounts.reserve(account, amount); // available -= amount, held += amount
return ledger.record("AUTH", account, amount);
}
public void capture(String authId) {
var r = ledger.find(authId);
accounts.settle(r.account(), r.amount()); // held -> debited
ledger.record("CAPTURE", r.account(), r.amount());
}
public void voidAuth(String authId) {
var r = ledger.find(authId);
accounts.release(r.account(), r.amount()); // held -> available again
ledger.record("VOID", r.account(), r.amount());
}
}
What is a double-entry ledger?
Every transaction writes a debit entry and a matching credit entry, so the signed sum is always zero. Refunds are new entries, not edits. This makes the ledger an immutable audit trail. Auditors at National Bank of India can trace every rupee movement without worrying about tampered records.
package com.bank.payment;
import java.math.BigDecimal;
import java.time.Instant;
public class LedgerService {
public void recordTransfer(String fromAccount, String toAccount, BigDecimal amount) {
String txnId = generateTxnId();
ledgerStore.append(new LedgerEntry(txnId, fromAccount, amount.negate(), Instant.now()));
ledgerStore.append(new LedgerEntry(txnId, toAccount, amount, Instant.now()));
// sum of all entries for txnId = 0
}
}
What is PCI DSS compliance?
The system never stores raw card numbers. When Rajesh enters his card, the number goes to a PCI-compliant vault that returns a token. The payment service uses only the token for authorization and capture. A breach of the payment database exposes tokens, not real card numbers.
package com.bank.payment;
public class CardTokenizer {
public String tokenize(String rawCardNumber) {
return pciVault.store(rawCardNumber); // returns "tok_abc123"
}
public String detokenize(String token) {
return pciVault.retrieve(token); // only called inside PCI boundary
}
}
How do you handle partial failures?
If the gateway succeeds but the local write fails, the bank has charged the customer without recording it. A reconciliation job compares gateway settlement files against the local ledger every night and creates correcting entries for any mismatches.
package com.bank.payment;
import java.util.List;
public class ReconciliationJob {
public void reconcile() {
List<GatewayRecord> gatewayRecords = gateway.fetchSettlement();
for (GatewayRecord gr : gatewayRecords) {
if (!ledger.exists(gr.transactionId())) {
ledger.createCorrectingEntry(gr); // repair the mismatch
alertOps(gr); // notify operations team
}
}
}
}
How do you handle refunds?
Refunds are new ledger entries crediting the payer and debiting the merchant, never edits to existing entries. This preserves the append-only audit trail. The original payment and the refund are both visible in the ledger, linked by a reference id.
What should a senior engineer know?
A senior engineer designs the reconciliation job as a first-class component, not an afterthought, and knows that the ledger, not the balance column, is the source of truth. They also plan for multi-currency support using a currency field on every ledger entry and exchange-rate snapshots at transaction time.
Visual Overview
Payment System Architecture
┌──────────┐ pay request ┌──────────────────┐
│ Client │──────────────►│ Payment Service │
│ (idem │ │ │
│ key) │ │ 1. Check idem │
└──────────┘ │ 2. Authorize │
│ 3. Capture │
│ 4. Ledger entry │
└────────┬─────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Accounts │ │ Ledger │ │ Gateway │
│ (balance, │ │ (append- │ │ (card │
│ version) │ │ only, │ │ network) │
└─────────────┘ │ double- │ └─────────────┘
│ entry) │
└─────────────┘
Ledger entries for a transfer:
┌──────────┬───────────┬──────────┐
│ Entry │ Account │ Amount │
├──────────┼───────────┼──────────┤
│ DEBIT │ SAV2001 │ -50000 │
│ CREDIT │ CUR3001 │ +50000 │
│ Sum │ │ 0 │
└──────────┴───────────┴──────────┘
Key Takeaway
A payment system moves money with correctness, durability, and auditability as its highest priorities. Idempotency keys make retries safe. Double-spend prevention uses atomic conditional updates. Two-phase authorize-then-capture matches card network flows. The append-only double-entry ledger ensures every debit has a matching credit and the books always balance.
PCI compliance means never storing raw card numbers. A reconciliation job repairs mismatches between the gateway and local ledger. At National Bank of India, this combination of mechanisms made the payment system trustworthy enough to move real money.
Remember one sentence: make payments idempotent, prevent double-spend atomically, use authorize-capture for funds reservation, and record everything in an append-only double-entry ledger.
---