System Design · Architecture under constraints

How would you design a payment system?

HardpaymentsledgeridempotencyUpdated Oct 2026
Quiz this track

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.

Do not store raw card data unless the system is in PCI scope. Use provider tokens and hosted fields. Minimize the blast radius of a compromise by keeping card data out of application logs and databases.

Follow-ups you should expect

Interviewers use these to check whether the first answer was understanding or memorisation.

  1. 1Why is a double-entry ledger better than a balance column?
  2. 2What does a timeout from the payment provider actually mean?
  3. 3How do you handle a webhook that arrives twice?
  4. 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.