System Design · Architecture under constraints
How would you design a payment system?
Short answer
Model money as an immutable double-entry ledger, drive each payment through an explicit state machine, use idempotency keys at every external boundary, and reconcile against the provider.
Assume card payments through an external provider, refunds, partial captures, and a requirement that the ledger always balances. Money is not a counter you increment; it is a set of entries whose sum must be explainable. The system must be auditable and must never double-charge on a retry.
Core model
- Double-entry ledger: every movement debits one account and credits another, and the sum is zero
- Payment state machine: created, authorized, captured, settled, refunded, failed, with allowed transitions
- Idempotency key on every create, capture and refund request
- Outbox for events so the ledger commit and downstream notification cannot diverge
- Provider adapter that translates internal states to the provider's API and webhooks
The provider is an external system with its own failure modes. A timeout does not mean failure; it means unknown. Store the provider's idempotency key and reconcile by polling or webhook before retrying. A webhook must be verified, deduplicated and applied idempotently, because it can arrive twice or out of order.
Reconciliation is not optional. Daily, compare the internal ledger against the provider's settlement report. Classify mismatches into missing, duplicate and amount errors, and route them to an operations queue. The reconciliation job itself must be idempotent and able to rerun for a date range without double-posting.
Isolation matters. Authorization and capture touch the same payment, so use a per-payment lock or conditional update to avoid concurrent capture and refund. The ledger should reject an entry that would violate an invariant, rather than trusting the service layer to be correct. Keep an append-only audit log of every state transition.
Follow-ups you should expect
Interviewers use these to check whether the first answer was understanding or memorisation.
- 1Why is a double-entry ledger better than a balance column?
- 2What does a timeout from the payment provider actually mean?
- 3How do you handle a webhook that arrives twice?
- 4How would you design refunds against a partially captured payment?
Say it in the interview
I model money with an immutable double-entry ledger and drive each payment through an explicit state machine, because balances alone cannot explain or audit a movement. Every external call carries an idempotency key, and a provider timeout means unknown, so I reconcile before retrying. Webhooks are verified, deduplicated and applied idempotently, and a daily settlement reconciliation catches mismatches. Card data stays out of the application entirely, using provider tokens.
Use this as a model, not a script. Saying it in your own words is what makes it survive follow-up pressure.
Related questions
Reading is step one
Now say it out loud.
Understanding an answer and delivering it under pressure are different skills. Rehearse this topic in a live System Design mock interview and get scored feedback on depth, clarity and confidence.