Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 14 – Case studies: money and commerce

Low-level design: payment gateway

Design a payment gateway in code: a payment state machine, idempotent create, capture and refund, provider adapters, and webhooks that come late or twice.

  • Advanced
  • 40 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free intro, Pro: 9 of 40 minutes free
Free: The prompt, the rules and the questions it must answer. With a pass: the other 10 sections and 3 interview answers, about 31 minutes.

What you will learn

  • Model payments, attempts, providers and webhooks with a clear state machine
  • Design idempotent create, capture and refund operations
  • Route between providers and handle late or duplicate callbacks
  • Compare policies for a success that arrives after a timeout, with numbers

Before you start

What you’ll learn · Before you start

What you will learn

  • Model payments, attempts, providers and webhooks with a clear state machine
  • Design idempotent create, capture and refund operations
  • Route between providers and handle late or duplicate callbacks
  • Compare policies for a success that arrives after a timeout, with numbers

Before you start

On this page

Designing a payment gateway in a low-level design (LLD) round means modelling what sits between a shop’s checkout and the companies that actually move money: the payment providers. A Payment moves through a small state machine, from processing to authorised and captured, and on to refunds in part or in full, or to failed. Each provider is reached through an adapter that turns its own status words into the gateway’s events. Three rules carry the design. Every request that moves money carries an idempotency key, so a retry returns the stored answer. Every webhook carries an event id, so a repeated one changes nothing. And silence is not failure: a payment whose provider has not answered is still processing, so a success that arrives late is reconciled, never charged a second time.

The prompt, the rules and the questions it must answer

The prompt: “Design the classes of a payment gateway that shops call to take card payments, with two providers behind it.” The clarifying answers that matter: shops create a payment, capture it once the order ships, and refund it in parts; providers answer some calls at once and report the rest by webhook, sometimes twice and sometimes out of order; and when the cheaper provider fails, payments should move to the other one.

  • G1 A payment is in exactly one state, and only the transitions of one table can change it.
  • G2 Each provider’s statuses are translated by its adapter; a status the adapter does not know sends the payment to review, never to success.
  • G3 Create, capture and refund carry an idempotency key: the same key and request return the stored answer, and the key sent with another request is refused.
  • G4 Webhooks are applied once per event id, and news of a state the payment has already passed is ignored.
  • G5 Refunds never add up to more than was captured, counting refunds still waiting for the provider.
  • G6 A timeout is an unknown outcome: the gateway asks for the status before anything else is charged.
  • G7 A success that arrives after the payment failed goes to review, and the money goes back to the customer.
  • G8 Calls go to the cheapest healthy provider for the method, and a failing provider is skipped for a while.
State diagram of a payment: processing, authorised, captured, refunded in part or in full, with failed and needs_review at the side.merchant: create(amount, key)processingsent to a providerauthorisedmoney reserved at the bankcapturedmoney takenpartially_refundedrefunded(final)failed(final unless news arrives)needs_reviewa person or a job decidesauthorisedcaptured(its authorisation not seen yet)capturedrefunded, partrefunded, the restrefunded, alldeclined, or the deadlinepassed without an answera status the adapterdoes not knowa success arrivesafter the failure

A payment's states and the events that move it

Text description of the diagram

The diagram shows the states of one payment, from top to bottom, and the event on each arrow.

  1. The merchant's create(amount, key) starts the payment in processing: it has been sent to a provider.
  2. From processing, the event authorised leads to authorised (the money is reserved at the bank), and the event captured leads straight to captured when the capture is reported before its authorisation.
  3. From authorised, captured leads to captured (the money is taken).
  4. From captured, a refund of part of the amount leads to partially_refunded, and a refund of the rest leads on to refunded, which is final. A refund of everything leads from captured straight to refunded.
  5. At the side, processing leads to failed when the payment is declined or its deadline passes without an answer. Failed is final unless news arrives.
  6. Processing leads to needs_review when a provider reports a status that its adapter does not know, and failed leads to needs_review when a success arrives after the payment failed. In needs_review a person or a job decides what happens next.

The sizes are modest for one gateway, but they decide what the stores must hold. Work out the first two answers below before you check them.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 A gateway handles 2 million payments a day. Each payment produces 3 webhooks on average, and 5 % of webhook deliveries are repeated once by the provider. About how many webhook deliveries arrive in a day, in millions?

    Type a number.

    Show the answer to question 1

    Answer: 6.3 million deliveries (anything from 5.67 to 6.93 counts)

    2 million × 3 = 6 million webhooks, and 5 % of them arrive a second time: 6 million × 1.05 = 6.3 million deliveries, or about 73 a second on average. Every one of the 0.3 million repeats must change nothing, which is why the inbox records each event id.

  2. Question 2 of 4 Merchants send 25 requests a second that move money, each with its own idempotency key, and the gateway keeps every key for 24 hours. About how many keys are in the store at any moment, in millions?

    Type a number.

    Show the answer to question 2

    Answer: 2.16 million keys (anything from 1.944 to 2.376 counts)

    25 × 86,400 seconds = 2.16 million keys. At about 300 bytes each (the key, a fingerprint of the request and the stored response) that is roughly 650 MB, which fits one database table with an index on the key and an expiry job.

  3. Question 3 of 4 A provider's webhook says a payment was captured, but the payment is still processing: the webhook for its authorisation has not arrived yet. What should the state machine do?

    Choose one answer.

    Show the answer to question 3

    Answer: Move the payment to captured, because a capture implies the authorisation, and treat the later authorisation as stale.

    Providers do not promise to deliver events in the order they happened. A capture can only follow an authorisation, so the payment moves forward to captured, and news of a state it has already passed changes nothing when it arrives.

  4. Question 4 of 4 A provider sends a status that its adapter has never seen. Which state should the payment move to?

    Choose one answer.

    Show the answer to question 4

    Answer: needs_review, so that a person or a job looks at it before anyone acts

    An unknown status is neither a success nor a failure. Treating it as success could ship goods that were never paid for; treating it as failure invites a second payment for money that may already be taken. Dropping it loses the only news the provider sent. needs_review keeps the payment out of both mistakes until someone checks.

The in-depth parts take the design through these questions, in the order an interviewer tends to ask them:

  1. Which classes own the state machine, and how do adapters keep the providers’ vocabularies out of it?
  2. What does each webhook do to a payment when it is repeated, late, out of order or in an unknown status? The same scenario is replayed in Python, JavaScript, Java and Go.
  3. How are create, capture and refund made idempotent from the shop to the provider? An idempotency key turns a retried request into a lookup of the stored response.
  4. A payment timed out, failed, and then succeeded. What should the gateway have done, and what does each policy cost? One policy fails at the deadline and ignores late news, one fails and refunds late successes, and one stays pending and asks the provider.
  5. How does routing pick a provider? It can follow cost, recent success rate or fixed weights. A circuit breaker stops calls to a failing provider and lets one trial through after a pause.
  6. Which points does each level of candidate need to make, and how should a 45-minute rehearsal of this design be spent?

The classes and the transition table Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • One scenario in four languages: duplicates, stale news and unknown statuses, replayed
  • Idempotent create, capture and refund, from the shop to the provider
  • A late success after a timeout: three policies, drilled with numbers
  • Routing with a circuit breaker per provider, and why a timeout never fails over
  • Practise: partial refunds and the daily report
    • Partial refunds within what was captured, and a daily report
  • What each level is expected to cover
  • A 45-minute self-mock
  • Interview questions
    • Why should a payment gateway translate each provider's statuses through an adapter instead of storing the provider's own status on the payment?
    • Your gateway marked a payment failed after a 30-second timeout and told the shop. Four minutes later the provider's webhook says the payment was captured. What should the gateway do now, and how would you design it so that this case is routine rather than an incident?
    • A shop retries POST /payments with the same idempotency key while the first request is still waiting for the provider. What should the second request get, and how do you implement that safely when the gateway runs on several servers?
    • The cheaper of your two providers starts failing a third of its calls, some with refusals and some with timeouts. How would you design routing with a circuit breaker so that payments move to the other provider without charging anyone twice?
  • Key takeaways

1 diagram · 5 runnable examples (Python, Replay.java, replay.go, replay.mjs, replay.py, replay_session.py, scenario.mjs) · 1 exercise with sample tests · 3 interview answers · about 25 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; nothing renews unless you choose to. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Low-level design: parking lot (the model)

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.