A payments system processes financial transactions — charge customers, transfer funds, process refunds. Unlike typical web applications, payments demand ACID guarantees at every step: a failed charge that was partially recorded leaves a user wrongly charged. Double-charge bugs destroy trust. The architecture must prevent duplicate charges, handle partial failures atomically, and maintain an auditable transaction ledger.
Idempotency Keys
The most critical property of a payments API: every charge endpoint must accept an idempotency key. The caller generates a UUID before making the API request and includes it as a header. If the request fails (network timeout, 500 error), the caller retries with the same idempotency key. The server checks the key: if this key has been used for a completed charge, return the original response without charging again. If this key is in-progress (concurrent request), wait or return 409. If this key is new, process the charge and record the response against the key.
Idempotency key storage: a payments_requests table with (idempotency_key, user_id, request_hash, response_body, status, created_at). Before processing: INSERT INTO payments_requests (key, status=processing) — if this INSERT violates the unique key constraint, the key exists and the previous response is returned. After processing: UPDATE status=complete, response_body=… This makes the entire flow atomic with respect to the idempotency key.
Double-Charge Prevention
Double charges occur when: (1) a payment is processed successfully but the response is lost in transit — the client retries, treating the timeout as a failure; (2) a bug triggers two concurrent charge attempts for the same order. Prevention: the idempotency key prevents (1) — retries return the original response. For (2): enforce a database-level unique constraint on (order_id, charge_type) in the charges table — the second charge attempt fails with a unique constraint violation rather than creating a duplicate charge. Application-level checks alone are insufficient (race conditions bypass them); the database constraint is the last line of defense.
Ledger Design
Financial systems use an append-only ledger — never update or delete transaction records. Every financial event creates a new ledger entry: account_id, amount (positive for credit, negative for debit), transaction_type, reference_id, created_at. The current balance is computed as SELECT SUM(amount) FROM ledger WHERE account_id = ?. This is intentionally slow for balance computation — balance is precomputed and cached in a balances table, with the ledger as the source of truth for auditing. Any discrepancy between cached balance and ledger sum indicates a bug. Run reconciliation jobs periodically to detect discrepancies.
Handling External Payment Providers
Calling Stripe, PayPal, or Braintree introduces external failures. Pattern: (1) create a pending charge record in your database before calling the external provider; (2) call the external provider API; (3) on success, update the charge record to completed and record the provider’s charge_id; (4) on failure, update to failed. If step 3 or 4 fail (network timeout after the provider charged the card), a reconciliation job compares your database against the provider’s transaction list and resolves discrepancies. Never trust the local database alone — always reconcile against the provider’s API daily.
Refund Design
Refunds must be idempotent: calling refund twice for the same charge must not result in two refunds. Pattern: each refund request gets an idempotency key; the refunds table has a unique constraint on (charge_id, refund_reason) or a caller-provided idempotency key. Partial refunds: the sum of all refunds for a charge must not exceed the original charge amount — enforce with a database constraint or application check backed by a SELECT FOR UPDATE lock on the charge record. Refund ledger: each refund creates a positive ledger entry for the customer and a negative entry for the merchant — the ledger always balances.
See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering
See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering
See also: Databricks Interview Guide 2026: Spark Internals, Delta Lake, and Lakehouse Architecture
See also: Anthropic Interview Guide 2026: Process, Questions, and AI Safety
See also: Atlassian Interview Guide
See also: Coinbase Interview Guide
See also: Shopify Interview Guide
See also: Snap Interview Guide
See also: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems