Commerce & payments · Advanced
Payment system with ledger
Idempotent payments API, processor routing and an append-only double-entry ledger.
Every part, and why it is there
Merchant backends
Online shops call the payments API from their servers. Their networks time out and they retry, so the API must treat a repeated request as the same request.
API gateway
Authenticates the merchant’s secret API key and applies per-merchant rate limits before any money logic runs.
Payments API
POST /v1/payment_intents creates a payment; confirm and capture move it through states (created → authorized → captured → refunded) and records every transition. It never trusts that a call to the outside world happened exactly once.
Idempotency keys
The merchant sends a unique key with each request. The first request stores the key with its result; a retry with the same key gets the stored result back instead of a second charge.
Risk scoring
Scores each payment for fraud in tens of milliseconds and can block or ask for 3-D Secure. It sits in the path, so it needs a strict timeout and a safe default if it is slow.
Processor router
Talks to card networks and banks through adapters, and can fail over between acquirers. Each outbound call carries its own idempotency key so the bank can dedupe our retries too.
Card networks / acquirers
The outside world that actually moves money. It is slow, sometimes down, and occasionally answers “unknown”, which is why the state machine has a pending state.
Payments DB
Holds the current state of every payment. Writing the state change and an outbox event in one transaction means the ledger can never miss a payment.
Payment events
An ordered log of state changes. Several consumers read it independently, so adding a new one (analytics, reporting) never touches the payment path.
Ledger service
Turns each event into balanced journal entries: money leaves one account and enters another. Entries are only ever added, never edited, so the history is auditable.
Double-entry ledger
Append-only rows of debits and credits that must sum to zero per transaction. Balances are derived from entries, so a bug shows up as an imbalance instead of silently lost money.
Reconciliation
Matches what we think happened against the processor’s settlement reports each day. Distributed systems drift; reconciliation is how you find out before the auditors do.
Webhook sender
Tells merchants about payment changes by calling their webhook URL, retrying with backoff. Delivery is at-least-once, so merchants must handle the same event twice.
Merchant webhook URLs
Merchants’ own servers, outside our control. Some are slow or broken, which is why webhooks are sent from a queue and never inline.
Asked in interviews at
- openai
- stripe
- adyen
Concepts used here
- Database indexes: Find one user among a million by reading 4 pages instead of 12,500. Build the index by hand, then see what it costs and why column order matters.
- Replication: A copy of your database is not a live mirror. Copies run behind, so you can read old data, and a crash can lose a save you were told was safe.
- Sharding: Split one table across many database machines. It buys room and speed, and makes every query and transaction that crosses machines harder.
- HTTPS: Follow one password typed on café Wi-Fi, first over plain HTTP, where anyone nearby can read it, then over HTTPS: a secret agreed in public, a certificate that proves who the server is, and encryption for everything after.
Learn more
- Designing Idempotent API Endpoints for Payments at Stripe (Arpit Bhayani)
- Design a Payment System - System Design Interview (Code with Lucian)
- [MoneyCon 2019] Controlling Our Own Destiny: Payments as a Service(s) at Airbnb (Uber Engineering)
- Designing robust and predictable APIs with idempotency (Stripe)
- Ledger: Stripe's system for tracking and validating money movement (Stripe)
- Avoiding double payments in a distributed payments system (Airbnb Engineering)
- Books: an immutable double-entry accounting database service (Square)
- Payment system (ByteByteGo)