Design a Payment Gateway
Payment intents, provider adapters, idempotency, webhooks, and a saga for capture/refund flows.
Problem Statement
Design the low-level object model for a payment gateway that accepts card, wallet, and UPI payments for merchants. The gateway must normalize different payment methods, route each payment to one of several downstream processors, execute authorize and capture, keep transaction state correct, make retries safe, and support refunds after a successful capture.
The interview focus is the boundary between gateway orchestration and provider integrations: use Strategy for choosing the right processor route, Adapter for translating each provider API into a common interface, and an explicit State model for the payment lifecycle.
Business context
Payment gateways sit between merchants and processors such as Stripe, PayPal, banks, wallet providers, and UPI rails. A strong LLD answer shows that money movement is not just another API call: it needs idempotency, safe retries, audit-friendly state transitions, clear provider seams, and refund behavior that cannot corrupt already captured money. Interviewers use this problem to evaluate whether a candidate can separate policy from integration details while defending critical invariants.
Functional Requirements
Accept card, wallet, and UPI payment methods with a normalized domain model.
Create a payment transaction with merchant id, amount, currency, method, and idempotency key.
Route payments to downstream processors based on payment method or routing policy.
Integrate multiple processors behind a common PaymentProcessor port using adapters.
Move each transaction through Initiated → Authorized → Captured, or into Failed when authorization or capture cannot complete.
Return the same transaction for repeated requests with the same idempotency key.
Retry retryable processor failures without creating duplicate provider charges.
Refund a captured payment and move it to Refunded while rejecting refunds from invalid states.
Non-Functional Requirements
Correct financial state
State transitions must be explicit and enforced. A payment cannot jump directly from Initiated to Captured, and a failed payment cannot later be refunded.
Idempotency
A merchant retry with the same idempotency key must return the original transaction rather than creating another provider authorization.
Provider isolation
Stripe, PayPal, bank, and UPI quirks must stay inside adapters. The gateway service should speak one common interface.
Reliability under transient failure
Retry only responses that are marked retryable and preserve the same transaction identity across attempts.
Auditability
Every status, provider reference, retry count, and refund decision should be explainable from the domain object and persisted in a real system.
Extensibility
Adding a new processor, method, or routing rule should be additive: new adapter or strategy class, not edits across the lifecycle code.
Requirement Clarification
QDo we need to model settlement, reconciliation, and webhooks?
Not in the base implementation. We model synchronous authorize, capture, refund, and note that webhooks and reconciliation are production extensions.
QShould authorize and capture be separate operations?
Yes. The lifecycle explicitly separates Authorized from Captured because many gateways reserve funds before capturing them.
QCan the same request be retried by the merchant?
Yes. The merchant supplies an idempotency key. Replays return the existing payment object and do not call the processor again.
QHow are processor failures classified?
The adapter returns a normalized response that marks failures as retryable or permanent. The service retries only retryable failures.
QCan a captured payment be refunded more than once?
For the base design, one full refund is supported. Partial and multi-refund workflows are covered as an extension.
UML Class Diagram
Sequence Diagram
Entity Identification
PaymentGatewayService
Application service and façade for merchants. It enforces idempotency, creates payments, picks the processor route, retries retryable failures, captures funds, and refunds captured transactions.
Payment
Aggregate root for one money movement. It stores merchant, amount, method, idempotency key, provider reference, retry count, failure reason, and current lifecycle status.
PaymentStatus
State model for legal transitions. It prevents invalid jumps such as Initiated directly to Captured or Failed to Refunded.
PaymentMethod
Normalized representation of card, wallet, and UPI instruments. It keeps the gateway independent of provider-specific request payloads.
PaymentProcessor
Common processor port. The gateway calls authorize, capture, and refund without knowing which provider sits behind the call.
StripeAdapter
Adapter that maps the common processor port to a Stripe-like card processor, including normalized response objects.
PayPalAdapter
Adapter that maps the common processor port to a PayPal-like wallet and UPI capable processor.
ProcessorSelectionStrategy
Routing policy abstraction. The base implementation maps payment method type to a configured processor, but the service only depends on the strategy interface.
Design Patterns Used
StripeAdapter and PayPalAdapter translate provider-specific behavior into the shared PaymentProcessor interface. Provider quirks never leak into PaymentGatewayService.
ProcessorSelectionStrategy owns routing. Today it routes by payment method; later it can route by country, cost, success rate, merchant preference, or traffic split without changing payment execution.
PaymentStatus encodes legal lifecycle moves. The domain object must ask the current state before changing, which protects captured, failed, and refunded invariants.
Static creation methods such as Payment.create, PaymentMethod.card, PaymentMethod.wallet, and PaymentMethod.upi centralize valid object construction and keep callers from assembling partial transactions.
Step-by-Step Design
1Start with the transaction lifecycle
Model money movement as a state machine before discussing processors. The state model is the guardrail that keeps retries, refunds, and failed captures honest.
public enum PaymentStatus { INITIATED, AUTHORIZED, CAPTURED, FAILED, REFUNDED; public boolean canTransitionTo(PaymentStatus next) { return (this == INITIATED && (next == AUTHORIZED || next == FAILED)) || (this == AUTHORIZED && (next == CAPTURED || next == FAILED)) || (this == CAPTURED && next == REFUNDED); } }2Normalize provider calls behind one processor port
The gateway should not contain provider request builders or response parsing. It should call a stable interface and receive a normalized response.
public interface PaymentProcessor { ProcessorResponse authorize(Payment payment); ProcessorResponse capture(Payment payment); ProcessorResponse refund(Payment payment); }3Split routing strategy from provider adapters
The strategy decides which processor to use. The adapter decides how to talk to that processor. Mixing those roles creates brittle switch statements.
public interface ProcessorSelectionStrategy { PaymentProcessor select(PaymentMethod method); }4Apply idempotency before side effects
Look up the idempotency key before creating or authorizing a payment. Store the new payment under the key before processor calls so a replay observes the same transaction identity.
5Retry only normalized retryable failures
Adapters classify responses as success, retryable failure, or permanent failure. The service retries retryable failures with the same Payment and stops immediately on permanent failures.
6Refund only from the captured state
Refund is not a generic reversal. The service rejects refunds unless the payment is Captured, calls the same selected processor adapter, and moves the state to Refunded only after processor success.
Complete Java Implementation
Explanation of Every Class
PaymentMethod
Normalized method abstraction for card, wallet, and UPI. Static factory methods keep construction concise while hiding the simple implementation class.
Payment
Transaction aggregate with the embedded PaymentStatus state machine. It stores request facts, mutable status, provider reference, retry count, and failure reason; all state changes go through lifecycle methods.
PaymentProcessor
Common processor port plus normalized ProcessorResponse. It lets the gateway treat authorization, capture, and refund consistently across providers.
StripeAdapter
Adapter for a Stripe-like card processor. It validates card routing, maps provider success to a provider reference, and classifies failures as retryable or permanent.
PayPalAdapter
Adapter for a PayPal-like wallet and UPI processor. It exposes the same PaymentProcessor methods even though the downstream provider has different capabilities.
ProcessorSelectionStrategy
Strategy interface and method-based implementation. It maps CARD to Stripe and WALLET or UPI to PayPal in the base design, but the gateway only depends on select.
PaymentGatewayService
Orchestrates the use case. It checks idempotency first, creates and stores the payment, asks the strategy for a processor, retries safe failures, advances state, and handles refunds.
Main
Small demo showing a card payment, an idempotent replay with the same key, a UPI payment routed to the other adapter, and a refund of the captured card payment.
Dry Run
Sample input
Merchant merchant-42 submits a 2599 INR card payment with idempotency key order-1001, retries the same request once, then asks for a refund. A second UPI payment uses key order-1002.
| Step | Action | Processor | Status | Idempotency store | Result |
|---|---|---|---|---|---|
| 1 | process card key order-1001 | StripeAdapter | INITIATED | {order-1001 → P1} | new payment P1 created |
| 2 | authorize P1 | StripeAdapter | AUTHORIZED | {order-1001 → P1} | provider reference stored |
| 3 | capture P1 | StripeAdapter | CAPTURED | {order-1001 → P1} | merchant receives captured payment |
| 4 | replay key order-1001 | none | CAPTURED | {order-1001 → P1} | same P1 returned, no processor call |
| 5 | process UPI key order-1002 | PayPalAdapter | CAPTURED | {order-1001 → P1, order-1002 → P2} | UPI routed by strategy |
| 6 | refund order-1001 | StripeAdapter | REFUNDED | {order-1001 → P1, order-1002 → P2} | P1 moves to refunded |
The dry run highlights the key interview invariant: replay happens before any adapter call, while refund is allowed only after capture.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| processPayment with new key | O(A) | O(1) | A is the configured retry attempt count; routing and state transitions are constant time. |
| processPayment with existing key | O(1) | O(1) | One hash-map lookup returns the already stored payment. |
| refund | O(A) | O(1) | Lookup, state check, processor refund call, and at most A attempts. |
| select processor | O(1) | O(1) | The method-based strategy uses an enum map. |
| findPayment | O(1) | O(1) | Direct lookup by generated payment id. |
The interesting cost is not CPU; it is correctness under remote calls. The gateway keeps local orchestration constant-time and pushes network latency, retry policy, and provider rate limits behind adapters.
Extensibility
New processor
Implement PaymentProcessor in a new adapter such as RazorpayAdapter and add it to the routing strategy configuration.
New payment method
Add a PaymentMethodType value and a factory method on PaymentMethod, then teach the strategy which processor supports it.
Advanced routing
Replace method-based routing with a strategy that considers merchant, country, currency, live success rates, cost, or circuit-breaker health.
Partial refunds
Introduce a refund aggregate with amount, status, and provider reference. Keep Payment captured until the total refunded amount reaches the captured amount.
Webhook reconciliation
Add provider webhook adapters that reconcile asynchronous authorization, capture, or refund events back into the same state machine.
Alternative Designs
One adapter per payment method
Create CardProcessor, WalletProcessor, and UpiProcessor interfaces instead of one PaymentProcessor.
Tradeoffs
Method-specific interfaces can expose richer capabilities, but merchants and gateway orchestration now depend on more ports and duplicate lifecycle logic.
Processor enum with switch statements
Represent providers as enum values and switch inside PaymentGatewayService for routing and provider calls.
Tradeoffs
This is quick for a demo but violates open-closed design: every new provider edits the central service and risks breaking existing routes.
Command objects for each operation
Represent authorize, capture, and refund as command objects that can be persisted and replayed by workers.
Tradeoffs
Useful for distributed production systems and outbox processing, but it adds infrastructure that can distract from the core interview model.
Common Mistakes
- ×
Putting Stripe and PayPal request details directly inside PaymentGatewayService instead of adapters.
- ×
Using one switch for card, wallet, UPI, and provider routing, which mixes Strategy and Adapter responsibilities.
- ×
Creating a new payment before checking the idempotency key, causing duplicate charges on merchant retry.
- ×
Treating retry as a blind loop without distinguishing retryable and permanent failures.
- ×
Allowing invalid state jumps such as Initiated to Captured or Failed to Refunded.
- ×
Refunding without checking that the payment is already captured.
- ×
Losing the provider reference needed to capture or refund with the downstream processor.
Follow-up Interview Questions
QHow would you support partial refunds?
Add a Refund entity with amount, idempotency key, status, and provider reference. A payment may have many refunds, and the aggregate enforces that total refunded amount never exceeds captured amount.
QWhat changes when capture is asynchronous?
Introduce CapturePending or an operation record, persist the request, and let provider webhooks move the payment to Captured or Failed. The same state machine remains the source of truth.
QHow do you prevent duplicate provider calls across multiple app instances?
Back the idempotency store with a database unique constraint on merchant id plus idempotency key and run creation plus state update in a transaction or with row-level locking.
QHow would routing prefer the cheapest healthy processor?
Implement a new ProcessorSelectionStrategy that reads health and pricing metadata, filters unhealthy processors, and returns the lowest-cost eligible adapter.
QWhere do webhooks belong in this design?
As inbound adapters that translate provider webhook payloads into domain events or service calls. They should not bypass the payment state machine.
Production Considerations
Persistent idempotency store
Use a durable table keyed by merchant id and idempotency key with a unique constraint. Store request hash and response snapshot to detect conflicting replays.
Outbox and retries
For real remote calls, persist operation intents and process them through an outbox worker so retries survive service restarts.
Provider reconciliation
Regularly compare gateway state with processor reports and webhooks. Payments can be locally failed while the provider eventually succeeds unless reconciled.
Security and compliance
Do not store raw card data. Store tokens, encrypt sensitive references, scope access tightly, and meet PCI requirements where card data is handled.
Observability
Emit metrics for authorization success rate, capture success rate, retry count, refund failure rate, provider latency, and idempotent replay volume.
Circuit breakers
Adapters should expose health to the routing strategy so a degraded processor is avoided before merchants see repeated failures.
What Interviewers Look For
Do they separate routing policy from provider translation, or do they put everything in one service?
Do they explain why idempotency must be checked before external side effects?
Do they use an explicit state machine instead of scattered status assignments?
Do they classify failures so retries are safe and bounded?
Do they preserve the provider reference needed for capture and refund?
Can they evolve the design toward partial refunds, webhooks, and multi-instance consistency without rewriting the core model?
Quiz
0/5 answered
1.Why is **ProcessorSelectionStrategy** separate from **StripeAdapter** and **PayPalAdapter**?
2.What is the safest first step in **processPayment**?
3.Which transition should the base state machine reject?
4.What should an adapter return for a provider timeout?
5.What does **PaymentMethod.card** demonstrate?
Practice Variants
Add partial refunds
AdvancedModel multiple refund records per payment, each with its own idempotency key and status, and enforce that total refunded amount never exceeds captured amount.
Add processor health based routing
ExpertReplace method-only routing with a strategy that considers provider health, currency support, success rate, and merchant preferences.
Add asynchronous webhooks
ExpertIntroduce webhook adapters that translate provider events into safe state transitions and reconcile pending captures or refunds.
Flashcards
Cheat Sheet
Entities: PaymentGatewayService, Payment, PaymentStatus, PaymentMethod, PaymentProcessor, provider adapters, ProcessorSelectionStrategy.
Lifecycle: Initiated → Authorized → Captured; Initiated or Authorized may move to Failed; Captured may move to Refunded.
Patterns: Adapter for providers, Strategy for routing, State for transaction lifecycle, Factory Method for safe construction.
Critical flow: check idempotency → create payment → select processor → authorize → mark authorized → capture → mark captured.
Retries: adapter classifies retryable vs permanent failure; service retries only retryable failures with the same payment identity.
Refund: allowed from Captured only; call selected processor and mark Refunded only after success.
Production upgrades: durable idempotency table, outbox retries, webhooks, reconciliation, partial refunds, circuit breakers, PCI-safe token storage.
References
- BookEnterprise Integration Patterns — Gregor Hohpe and Bobby Woolf
- DocsRefactoring Guru — Adapter Pattern
- DocsStripe API Docs — Idempotent Requests
- DocsPCI Security Standards Council — PCI DSS