Why Payment Systems Are Hard
Payment systems are among the hardest distributed systems to design because money movement requires exactly-once semantics — double-charging a customer or double-paying a merchant is catastrophic. At the same time, the system must be highly available (downtime = lost revenue) and process thousands of transactions per second globally. This question appears at Stripe, PayPal, Square, Shopify, and any company processing payments.
Card Payment Flow
A card payment involves five parties: Customer, Merchant, Payment Processor (Stripe), Acquiring Bank (merchant bank), and Issuing Bank (customer bank). The flow: (1) Customer enters card details; the browser sends them to the payment processor over TLS — card details never touch the merchant server. (2) Processor sends an authorization request to the card network (Visa/Mastercard), which routes it to the issuing bank. (3) Issuing bank checks fraud rules, available credit, and approves or declines. Authorization hold is placed on the customer account. (4) Processor returns success/failure to the merchant. (5) At end of day, the merchant triggers capture (settlement): actual funds move from issuing to acquiring bank via the card network. Authorization without capture is common for hotels and rentals (hold then capture actual amount).
Idempotency: The Core Design Challenge
Network failures between any parties create a critical problem: a charge request may succeed at the bank but the response never reaches the processor. If the processor retries naively, the customer is charged twice. Solution: idempotency keys. The merchant generates a unique ID per payment attempt (UUID or Snowflake ID) and sends it with every request. The processor stores the key + result atomically. On retry with the same key, the processor returns the stored result without re-executing. This guarantees exactly-once charging regardless of network failures or client retries.
# Idempotency key implementation
def create_charge(amount, currency, payment_method, idempotency_key):
# Check if we already processed this key
existing = db.query(
"SELECT result FROM idempotency_keys WHERE key = %s",
idempotency_key
)
if existing:
return existing.result # return stored result, no re-execution
# Process the charge
result = card_network.authorize(amount, currency, payment_method)
# Atomically store key + result
db.execute(
"INSERT INTO idempotency_keys (key, result, created_at) VALUES (%s, %s, NOW())",
idempotency_key, result
)
return result
Double-Entry Bookkeeping
Every financial system uses double-entry bookkeeping: every transaction has a debit and a credit that sum to zero. This creates an audit trail and makes it mathematically impossible to create or destroy money. Schema: ledger_entries(id, account_id, amount, direction, transaction_id, created_at). A payment from customer to merchant creates two entries: debit customer account -$100, credit merchant account +$100. The system balance always sums to zero; any discrepancy indicates a bug. This is why financial systems use long/integer cents rather than floating point — floating point arithmetic is not commutative and can introduce rounding errors in large-scale aggregations.
Fraud Detection
Fraud detection at Stripe uses a three-layer approach: (1) Rule-based checks at transaction time (under 100ms): velocity limits (more than 3 failed cards from same IP in 10 minutes = block), card BIN blacklists, billing address mismatch. (2) ML model scoring: a gradient boosting model trained on billions of transactions produces a fraud probability score per transaction. Features: device fingerprint, IP geolocation vs billing address, transaction amount vs historical average, time-of-day, merchant category. Score threshold determines approve/review/decline. (3) Post-transaction monitoring: a batch job looks for patterns invisible at transaction time — coordinated fraud rings, account takeover sequences. Disputed charges (chargebacks) feed labeled training data back to the ML model.
Settlement and Reconciliation
Settlement moves actual funds between banks. The card network batches all authorized transactions from the day and runs settlement at the end of the business day. The acquiring bank receives funds from the issuing bank (minus interchange fees paid to the issuing bank and card network fees). The processor takes its processing fee from the merchant payout. Payout to merchant happens on a schedule (T+1 or T+2). Reconciliation is the process of verifying that the processor ledger matches the card network settlement reports — every dollar in must equal every dollar out, accounting for fees, refunds, and chargebacks. Automated reconciliation jobs run daily; discrepancies trigger alerts for manual review.
Database Design for Payments
Payment systems require strong consistency for money movement. Use a relational database (PostgreSQL) with ACID transactions. Key tables: payments(id, merchant_id, customer_id, amount_cents, currency, status, idempotency_key, created_at), payment_methods(id, customer_id, card_token, last4, expiry, billing_address), ledger_entries(id, payment_id, account_id, amount_cents, direction, created_at). Money amounts are always stored as integers (cents) to avoid floating point. Payment state machine: CREATED -> PROCESSING -> AUTHORIZED -> CAPTURED -> SETTLED. Refunds create a new payment record pointing to the original, with reversed ledger entries. Never modify historical ledger entries — append only.
Interview Tips
- Idempotency keys are the key insight — mention them immediately when the problem is stated
- Double-entry bookkeeping shows financial systems literacy
- Store amounts in integer cents, never floats — this always impresses interviewers
- Explain the card network parties: issuer, acquirer, card network, processor
- Fraud detection: rule-based (fast) + ML scoring (accurate) + batch monitoring (retroactive)
Frequently Asked Questions
How does a payment processor prevent double-charging?
Payment processors prevent double-charging using idempotency keys. The merchant generates a unique ID per payment attempt (UUID or timestamp-based) and includes it with every API request. When the processor receives a charge request, it first checks whether it has already processed a request with that idempotency key. If a matching key exists, the processor returns the stored result from the first request without re-executing the charge. This means network failures and client retries are safe: if the client never received the success response and retries with the same key, no second charge occurs. The idempotency key plus result are stored atomically in the database so the check-and-store cannot be split by concurrent requests.
What is the difference between payment authorization and capture?
Authorization is a temporary hold on funds — the issuing bank verifies the card is valid and the customer has sufficient funds, then reserves that amount. The merchant account does not receive the money yet. Capture is when the actual funds transfer occurs, typically in a batch settlement at end of day. The two-step auth/capture model allows merchants to authorize at order time and capture only when they ship (common in e-commerce). Hotels authorize on check-in for an estimated amount and capture the actual amount on checkout. Capture must occur within the authorization window (typically 7 days for Visa, 30 days for Amex). After this window, the auth expires and must be re-authorized. A void cancels an authorization before capture without moving any funds.
How does fraud detection work in a payment system?
Payment fraud detection operates in three layers: (1) Real-time rules (under 100ms, blocks the transaction): velocity checks (more than 3 failed card attempts from the same IP in 10 minutes = block), card BIN blacklists, CVV/AVS mismatch, impossible travel (card used in New York and London within 30 minutes). (2) ML scoring (runs synchronously during authorization): a gradient boosting model scores each transaction on fraud probability using features like device fingerprint, IP geolocation vs billing address, transaction amount vs customer history, merchant category risk, and time of day. High-score transactions trigger step-up authentication (3D Secure challenge). (3) Post-transaction monitoring: daily batch jobs analyze transaction graphs for coordinated fraud rings and account takeover patterns. Chargebacks (disputed transactions) provide labeled fraud examples for continuous model retraining.
{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “How does a payment processor prevent double-charging?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Payment processors prevent double-charging using idempotency keys. The merchant generates a unique ID per payment attempt (UUID or timestamp-based) and includes it with every API request. When the processor receives a charge request, it first checks whether it has already processed a request with that idempotency key. If a matching key exists, the processor returns the stored result from the first request without re-executing the charge. This means network failures and client retries are safe: if the client never received the success response and retries with the same key, no second charge occurs. The idempotency key plus result are stored atomically in the database so the check-and-store cannot be split by concurrent requests.” } }, { “@type”: “Question”, “name”: “What is the difference between payment authorization and capture?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Authorization is a temporary hold on funds — the issuing bank verifies the card is valid and the customer has sufficient funds, then reserves that amount. The merchant account does not receive the money yet. Capture is when the actual funds transfer occurs, typically in a batch settlement at end of day. The two-step auth/capture model allows merchants to authorize at order time and capture only when they ship (common in e-commerce). Hotels authorize on check-in for an estimated amount and capture the actual amount on checkout. Capture must occur within the authorization window (typically 7 days for Visa, 30 days for Amex). After this window, the auth expires and must be re-authorized. A void cancels an authorization before capture without moving any funds.” } }, { “@type”: “Question”, “name”: “How does fraud detection work in a payment system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Payment fraud detection operates in three layers: (1) Real-time rules (under 100ms, blocks the transaction): velocity checks (more than 3 failed card attempts from the same IP in 10 minutes = block), card BIN blacklists, CVV/AVS mismatch, impossible travel (card used in New York and London within 30 minutes). (2) ML scoring (runs synchronously during authorization): a gradient boosting model scores each transaction on fraud probability using features like device fingerprint, IP geolocation vs billing address, transaction amount vs customer history, merchant category risk, and time of day. High-score transactions trigger step-up authentication (3D Secure challenge). (3) Post-transaction monitoring: daily batch jobs analyze transaction graphs for coordinated fraud rings and account takeover patterns. Chargebacks (disputed transactions) provide labeled fraud examples for continuous model retraining.” } } ] }