Learning pathsA
GUIDED PRACTICE

Payment System

A payment service must represent uncertainty without creating another charge.

Interview scope and guarantees

Create payment intent, authorize or capture money through a provider, record a balanced ledger, support refunds and reconcile outcomes. Define currencies and decimal representation. External payment effects cannot be rolled back with a local transaction.

Capacity worksheet

Assume 10 million payments/day: about 116/s average and 1,157/s at 10× peak. With 500 ms mean provider latency, the peak requires about 579 concurrent provider calls. Ledger storage and audit retention grow with entries per payment, not just payment count.

Concrete API contract

Contract / pseudocode
POST /payments {merchantRequestId,amountMinor,currency,paymentToken}
GET /payments/{id}
POST /payments/{id}/capture {requestId}
POST /payments/{id}/refunds {requestId,amountMinor}

Data model and access paths

Contract / pseudocode
payments(id PK,merchant_id,request_key,request_hash,amount_minor,currency,state,provider_ref); UNIQUE(merchant_id,request_key)
operations(id PK,payment_id,type,idempotency_key,amount_minor,status); UNIQUE(payment_id,type,idempotency_key)
provider_attempts(id PK,operation_id,provider_key UNIQUE,state,provider_ref,next_check_at)
provider_events(provider,event_id,payload_hash); UNIQUE(provider,event_id)
ledger_transactions(id PK,operation_id UNIQUE)
ledger_lines(transaction_id,line_no,account_id,currency,amount_minor); PK(transaction_id,line_no)

Evolve a solution and explain each change

Three architecture decisions for Payment System, including the pressure each introduces.
Scroll to inspect the diagram, or open it at full size.

Figure — Three architecture decisions for Payment System, including the pressure each introduces.

Step 1: Record business intent

Create a payment identity and exact amount/currency before contacting a provider. A timeout cannot determine whether money moved.

Step 2: Use durable workflow

Persist attempts and stable provider keys; verify callbacks and reconcile uncertain results. Retries with new identities can charge twice.

Step 3: Keep an auditable ledger

Represent monetary postings and reversals immutably; distinguish authorization, capture, and settlement. A mutable status field alone is not complete accounting.

Responsibility overview

Connected responsibilities for Payment System. Trace the authoritative and derived paths separately.
Scroll to inspect the diagram, or open it at full size.

Figure — Connected responsibilities for Payment System. Trace the authoritative and derived paths separately.

Worked end-to-end scenario

Order O requests a 25-dollar charge with payment p17. The provider accepts attempt a1, but the connection drops. The service records unknown and retries using the same provider idempotency key or queries status. It does not create p18. A signed callback later reports success twice; deduplication and legal state transitions produce one capture record. A refund is a new referenced monetary operation, not deletion of the original charge. Reconciliation compares internal postings against provider reports and flags missing or contradictory outcomes for controlled repair.

Why these access paths matter

Unique business operation identity prevents repeated charge intent. Attempts are separate from payment state; provider event IDs deduplicate callbacks. Ledger entries record exact currency-specific amounts and balanced postings under the chosen accounting model. Index unknown attempts by next reconciliation time. Hold a database transaction only for local state, not across a slow external network call.

Build the baseline first

Payment System: baseline request paths.
Scroll to inspect the diagram, or open it at full size.
  1. Payment API → intent and operation → outbox.
  2. Worker → provider with stable key → result record.
  3. Ledger transaction → balanced entries → reporting.

Evolve the design under load

Payment System: additional scaling and recovery paths.
Scroll to inspect the diagram, or open it at full size.
  1. Merchant partitions → bounded provider dispatch.
  2. Signed webhooks → dedup and transition checks.
  3. Settlement reports → reconciliation → exception workflow.

Defend the hardest decision

Bind an idempotency key to the merchant and request fingerprint. Reusing a key with different amount or currency must fail. Model created, pending, authorized, captured, failed and uncertain outcomes deliberately; a transport timeout is uncertain, not failed. For each currency, ledger debits and credits balance within one transaction. Mutable account balances can be projections, but immutable entries provide the audit trail.

Failure and recovery analysis

A provider captures funds and the worker crashes before recording success. Retry or query using the same provider key; then insert the ledger transaction under a unique operation ID. A refund is another durable operation with its own identity and can fail independently. Prevent total refunds from exceeding captured value using an authoritative conditional check.

Security and privacy boundary

Tokenize payment credentials with the provider, restrict ledger writes, verify webhook signatures, and audit every manual correction.

Interview follow-ups with reasoning

Question: How do you handle webhook-before-response?

Show answer and explanation

Answer: Both converge through the same idempotent state transition.

Question: How do you recover after an outage?

Show answer and explanation

Answer: Reconcile uncertain operations and settlement totals before issuing new attempts.

Question: How do you migrate a ledger?

Show answer and explanation

Answer: Dual-validate balances and retain a reversible cutover boundary.

Operate and verify the design

Uncertain payment age, duplicate-key conflicts, unbalanced ledger attempts and settlement discrepancies.

Drop the capture response, retry with the same key and assert one provider effect and one ledger operation.

A second scenario to test transfer

A customer pays 2,500 minor units. The provider accepts the charge but the response is lost. The UI shows processing, not a definitive failure that encourages a new charge. The reconciler looks up the original provider identity, confirms success, appends the accounting event once, and updates the intent. A duplicate webhook finds the existing event identity and changes nothing.

A charge request times out. Is it safe to issue another charge with a new key?

Show answer and explanation

Answer: Not without resolving the first outcome. A new key denotes a new operation and can create a second charge. Query or retry using the original supported identity, then reconcile before deciding on a new business action.

Compare alternatives

ConcernProtectionOperational signal
Ambiguous chargeStable operation identityUnknown-state age
Duplicate callbackUnique inbox eventDuplicate rate
Missing callbackScheduled reconciliationUnmatched provider records
CorrectionNew ledger entriesBalance discrepancies

A design-changing exercise

Can a database rollback undo a provider charge?

Show answer and explanation

Answer: No. The provider is outside the local transaction. Recover through idempotency, status reconciliation, and an explicit compensating refund when appropriate.

Design workshop: a payment state and a balanced journal

Core scope is intent, authorization, capture, refund and reconciliation in one currency per payment. FX, disputes and merchant payouts are extensions. Choose durable intent acceptance, exact minor-unit amounts and no duplicated logical monetary operation. Provider/network uncertainty can delay finality; latency targets cannot turn unknown into failed.

Accept unique(merchant_id,request_key) with fingerprint over amount, currency and business identity. Same key/same fingerprint returns the original payment; changed fingerprint returns 409. Each authorize/capture/refund operation has its own stable identity and provider attempt key. Record provider inbox events uniquely and verify signatures. Do not store raw card secrets; accept tokenized provider references.

Authorization reserves provider-side funds but is not captured accounting. For an illustrative captured $25 payment, post +2,500 minor units debit to processor receivable and −2,500 credit to merchant payable. Sign convention is debit positive, credit negative. Sum must be zero within the currency for every transaction; entries in different currencies cannot offset each other. On a $10 refund, reverse the corresponding obligation: debit merchant payable 1,000, credit processor receivable 1,000. Fees and settlement use separate explicit lines.

Double-entry example in USD minor units. Capture C1 / Processor receivable +2,500 / Merchant payable -2,500; sum 0; Refund R1 / Merchant payable +1,000 / Processor receivable -1,000; sum 0; Duplicate C1 event / No new transaction under operation identity / Balance unchanged; Settlement extension / Cash received +net; fee expense +fee / Processor receivable -gross; total 0
Scroll to inspect the diagram, or open it at full size.

Figure — Double-entry example in USD minor units.

Enforce balanced posting through one trusted journal-writing transaction/service. Insert transaction header unique(operation_id), insert all lines, verify currency-specific sum and amounts, then mark posted; no reader treats incomplete lines as a committed journal. A balance check across rows needs a transaction/deferred validation or controlled writer, not a per-row CHECK pretending to sum unrelated rows.

sql
BEGIN;
SELECT captured_minor,refunded_minor,reserved_refund_minor
FROM payments WHERE id=:payment FOR UPDATE;
-- Return existing refund identity before checking remaining amount.
-- Require requested <= captured-refunded-reserved_refund.
-- Reserve amount and insert refund operation + provider outbox together.
COMMIT;

Refund reservation prevents two concurrent $15 refunds from spending the same $25 capture. Unknown refunds keep their reservation until reconciled; definitive failure releases it once; confirmed refund moves it from reserved to refunded and posts the journal once. Total successful+pending reserved refunds cannot exceed captured funds. A new refund request key does not safely resolve an uncertain previous refund.

Capture timeout does not create a second charge. Merchant to Payment authority: Payment P/request K; create capture C1; Payment authority to Provider: Invoke provider with stable key C1; Provider to Payment authority: Response lost after effect may have occurred; Payment authority to Merchant: Return processing/unknown and original P; Reconciler to Provider: Query/retry original supported C1 identity; Reconciler to Payment authority: Verified success -> one journal transaction for C1
Scroll to inspect the diagram, or open it at full size.

Figure — Capture timeout does not create a second charge.

Legal states distinguish intent, authorization_pending, authorized, capture_pending, captured and failed, with unknown attempt state where a result is ambiguous. Settlement is separate from capture. Multiple captures, if supported, each reserve part of authorized remaining amount under a payment lock and have their own identity. A terminal capture failure cannot override a verified captured outcome from another authoritative provider event.

Callbacks can arrive twice or out of order. Apply event IDs once, but also reconcile the current provider object/operation state; event arrival order is not state order. Provider key retention is finite and provider-specific. After that horizon, do not blindly retry a monetary request under a recycled key; query original object/settlement evidence and escalate unresolved ambiguity. Reconciliation compares operation totals, journal postings and provider settlement reports and appends corrections with auditable reasons.

Exercise: Capture is $25. Two $15 refund requests race and one accepted refund later times out. May the second use a fresh key and proceed?

Show answer and explanation

Answer: No. The first reserves $15, leaving $10 refundable. Its timeout remains unknown/reserved until resolved. Reusing the original identity/status avoids a second effect; a fresh key represents another operation.

Stripe idempotent requests and webhooks document one provider's boundaries. Balanced accounting and refund reservation are application invariants.

Technical references

Provider idempotency contract example.

PostgreSQL transaction isolation and concurrent updates.

11:00Self-guided practice timer
The timer resets when you leave this page. Save your design separately.
Your challenge

A charge request times out. Is it safe to issue another charge with a new key?

Your design draft

Clarify assumptions, explain your approach, and test the difficult cases. Save your draft, then compare it with the study notes.

Read study notes

Self-review checklist

Self-guided practice. Automated AI feedback and code execution are not connected.