Design a payment system like Stripe.

The complete Staff+-level answer — built from the same question definition the interview simulator probes, and scored against the same rubric it grades with.

HARD ~25 min read fintechconsistencyidempotency

Last updated · built from the live interview engine’s question definition and rubric

Two ways to read this. Night before the interview: the revision sheet plus the one-line checkpoint that closes each section. With a weekend: read straight through — the instruments are optional depth, not required reading.

Before you read

The answer in four anchors

Use these as the mental checklist while you skim; the sections below unpack each one.

Contract

Never charge twice

Say the contract before the boxes: “a retried request must never charge twice.” Everything below exists to honor that sentence.

Scale read

Correctness beats throughput

~1,160 TPS average, ~3,500 peak. Derive it once, conclude “correctness problem,” then stop doing math.

Core commit

State + ledger + outbox

Everything hangs off one commit: state + ledger + outbox in a single ACID transaction. Async begins only after it succeeds.

Deep dives

Name the seam

Every deep dive is the same move: name the seam, give the mechanism, bound the damage.

01 · The brief · minute zero

What you are handed

Interviewer brief — verbatim

Design a payment processing system like Stripe — accept payments, manage merchant accounts, and handle refunds with zero tolerance for double-charging.

Scale: 100M transactions/day, 99.999% availability requirement, multi-currency (150+ currencies), 50ms p99 for payment authorization.

Key challenges: idempotency for exactly-once payment processing, double-entry bookkeeping ledger, saga pattern for multi-step payment flows, PCI DSS compliance for card data, reconciliation with external payment providers, and handling partial failures (charged but not confirmed).

Decide: synchronous vs async payment processing? Real-time authorization vs batch settlement?

The shape — hold this before any detail

ACID money core

Idempotent charge, one transaction, double-entry ledger. Cannot lose or duplicate money.

Async shell

Webhooks, reconciliation, notifications — everything that tolerates a little delay.

External PSP + banks

Card auth and settlement. You orchestrate the call — you never trust it blindly.

client → gateway → payment service ⇄ PSP  ·  one ACID txn → ledger + outbox → Kafka → webhooks / recon

Three zones, and the whole interview is fought in the seams between them. The full diagram is in §06 — but draw this much in the first two minutes, so the interviewer has a model to hang everything else on.

What is really being asked

This is not a scale problem — 100M transactions/day is ~1,160 TPS, which a well-indexed Postgres absorbs. Every hard constraint in the brief is a correctness constraint: zero tolerance for double-charging, five nines, “charged but not confirmed.” The question rewards engineers who treat money as a contract, not data — that is nearly verbatim how the grading guidance is written.

Winning shape: a small, boring, ACID core that cannot lose or duplicate money, wrapped in an asynchronous shell for everything that tolerates delay. Candidates fail in the seams — retries, partial failures, the gap between your database and the PSP’s — not in the boxes.

02 · Requirements · minutes 0–8

Four functional requirements, six constraints

Functional

  1. Merchant submits a payment request for a customer.
  2. Merchant initiates a full or partial refund.
  3. System reconciles internal ledger with PSP settlement files.
  4. System notifies merchants of payment state changes via webhooks.

Reconciliation and webhooks look like afterthoughts and are not: reconciliation is how you prove the ledger is correct; webhooks are how merchants learn payment outcomes at all.

Non-functional

Exactly-once processing, stated as a contract: a retried request must never charge twice. The single most important requirement — say it unprompted.

Strong consistency on the money path, eventual elsewhere. Transaction record and ledger entries agree at every instant; dashboards, analytics, and notifications may lag.

99.999% availability ≈ 5 minutes/year. In tension with strong consistency — say so out loud.

Latency: budget it, don’t promise it. The brief’s 50ms p99 authorization figure can only be your own overhead — the PSP + issuing-bank leg alone is 200–400ms; end-to-end auth lands under ~500ms p99. Distinguishing the synchronous auth path from asynchronous settlement is a senior signal.

PCI DSS scope: raw card data never enters your systems; tokenize at the ingress. Frame as boundary minimization, not compliance recital.

Auditability: 7-year retention, immutable trail of every state change. Aside: GDPR right-to-delete conflicts with financial retention; retention wins for transaction data — staff-level nuance.

Scope cuts to state explicitly

Subscriptions and recurring billing; marketplace payouts; fraud-ML internals (place a fraud service, skip its model); full multi-currency treasury (quote in 150+ currencies; FX risk is a deep dive).

Checkpoint 02

Say the contract before the boxes: “a retried request must never charge twice.” Everything below exists to honor that sentence.

03 · Back-of-envelope · minutes 8–13

Numbers you should be able to re-derive

These are fixed engine reference figures, not a calculator. Each result follows from the arithmetic next to it.

Quantity Derivation Result
Average TPS 100M / 86,400s ~1,160 TPS
Peak TPS ~3× daily average; Black Friday ~10× ~3,500 / ~12K TPS
Ledger entries ≥2 per transaction (debit + credit); fees/platform cuts → 4–6 ~200M+ rows/day
Storage ~1KB/transaction → 100 GB/day → 36.5 TB/yr, 7-year retention ~256 TB raw
Webhook events ~3 lifecycle events per transaction, before retries ~300M/day
Idempotency keys ~100M keys/day at 24h TTL; ~256B/key (≈25 GB), more with cached responses ~25–60 GB in Redis

What the numbers mean — the conclusions, not the arithmetic

  • DB choice is driven by ACID, not throughput — say the conclusion, not the long division.
  • One payment is 5–10 internal operations → ~35K internal ops/sec at peak.
  • 100 GB/day forces storage tiering + partitioning from day one.
  • The 10× Black Friday spike arrives faster than autoscaling’s 2–3 minute warm-up — peaks are pre-provisioned.
  • Reference rates: refunds ~2–5% of transactions; chargebacks ~0.1%.
Checkpoint 03

~1,160 TPS average, ~3,500 peak. Derive it once, conclude “correctness problem,” then stop doing math.

04 · Core entities · minutes 13–17

Five entities and one state machine

Payment

payment_id, merchant_id, amount in minor units, currency, psp_reference, idempotency_key, status as a state machine.

LedgerEntry

Append-only double-entry: every movement is ≥1 debit + 1 credit summing to zero. No UPDATE, no DELETE; corrections are reversing entries. Balances are derived, never source-of-truth.

PaymentMethod

PSP token + display metadata — brand, last4, expiry. Never PAN, never CVV.

Merchant

Multiple API keys for rotation and test/live, webhook endpoint + signing secret, fee schedule.

Event log

Append-only record of ALL state changes including non-financial: fraud scores, retry attempts, webhook deliveries. Distinct from the ledger, which records money movements only — conflating them is a listed misconception.

The payment state machine

step with ← → or click a state

failure exits

Entry — CREATED

Merchant submits POST /v1/payments; the request is accepted and recorded.

Exit

Handed to the payment service → PROCESSING.

What candidates get wrong here

Treating payment as a single synchronous step — auth, capture, and settlement fail independently on different timescales.

Recurring follow-ups: “payment stuck in PROCESSING for 10 minutes?” (timeout state + sweeper) and “void vs refund?”.

Checkpoint 04

The ledger is append-only and balances are derived. A mutable balance column is the fastest no-hire in this question.

05 · API design · minutes 17–20

Three endpoints and one header that carries the interview

POST /v1/payments              Idempotency-Key: <client-generated>
POST /v1/payments/:id/refunds
GET  /v1/payments/:id

Merchant identity from the API key — never merchant_id in the body. Amounts in minor units. Responses mask card data to last4. Refunds: POST, never DELETE (a refund creates a financial event); validate cumulative refunds ≤ captured amount; refunds take idempotency keys too.

Idempotency-Key semantics — all four cases

Pick a case: what does the server do?

New key

Process normally; store key → response (24h TTL).

Show all four cases as a table
Case Behavior
New key Process normally; store key → response (24h TTL).
Retry: same key + same params Replay the stored original response.
Same key, request still in flight Don’t process twice, don’t stay silent — return a conflict (409) or block briefly.
Same key, different params Reject as a validation error — the merchant has a bug; don’t guess intent.

Keys are scoped per merchant — cross-tenant collisions must be impossible.

Webhooks — the five decisions that matter:

  • Typed events: payment.authorized / captured / refunded …
  • HMAC-signed — unsigned webhooks invite spoofed “payment.succeeded”.
  • At-least-once with exponential backoff + DLQ; the receiver dedupes by event ID.
  • No ordering promise — the payload carries a sequence number.
  • Version the API from day one — /v1 path or a Stripe-style version header.
Checkpoint 05

One header — Idempotency-Key — and four cases. If you can recite the four cases, the API section is won.

06 · High-level design · minutes 20–30

The living architecture

One architecture diagram, left to right — the shape you're drawing toward on the whiteboard. Play a payment through the happy path, or click any component to see what interviewers listen for there.

synchronous money path durable state async shell external
SYNCHRONOUS MONEY PATHEXTERNALEXTERNALDURABLE STATE — ONE ACID TRANSACTIONASYNC SHELLPOST /paymentsauthed requestatomic claimrisk scoreauthorizecard tokencard authone ACID txnentriesoutbox rowspublisheventseventsClient / Merchant SDKmerchant integrationAPI Gatewayauth · rate limitsPayment Servicestateless orchestratorPSP Gatewaycircuit breaker · routingExternal PSPsStripe / AdyenIdempotency StoreRedis · 24h TTLFraud Servicesync score · own budgetToken VaultHSM-backedLedger Servicedouble-entry · append-onlyPostgreSQLpayments · ledger · outboxOutbox RelayCDC / pollerKafkapartitioned by payment_idWebhook Deliveryretries · HMAC · DLQReconciliationnightly · settlement files
trace · happy path— / 6
Every component as plain text (all inspector notes)
Client / Merchant SDK — external
Submits payments with a client-generated Idempotency-Key. Merchant identity comes from the API key — never merchant_id in the body. Amounts in minor units; responses mask card data to last4. Refunds are POST, never DELETE — a refund creates a financial event. Refunds take idempotency keys too. Failure mode: Forbidden flows: the client must never access the DB directly, must not call the PSP directly (bypasses fraud and ledger), and must never write the ledger directly.
API Gateway — synchronous money path
Authenticates the API key and applies per-merchant rate limits. Every request enters here — the client must hit the gateway/LB, never direct to services. Merchants hold multiple API keys for rotation and test/live. Idempotency keys are scoped per merchant — cross-tenant collisions must be impossible. Failure mode: The gateway should not bypass the payment service to query the DB.
Payment Service — synchronous money path
Stateless orchestrator: atomic idempotency claim, fraud check before authorization, PSP call, then ONE ACID transaction writing the state transition + ledger entries + outbox event. Orchestration over choreography for money. Saga state is persisted, so recovery survives orchestrator crashes. Failure mode: The separating probe: the compensation fails too — PSP authorized, ledger write failed, void now rate-limited. Answer: recovery worker with backoff, stuck-saga sweeper with TTL escalation, ~7-day card-auth expiry as the bounded fallback.
Idempotency Store — synchronous money path
Key → outcome mapping with 24h TTL. The claim must be atomic (Redis SETNX / unique constraint) — read-then-write has a TOCTOU race two concurrent retries will find. Sized: ~100M keys/day × ~256B ≈ 25 GB, more with cached responses (~25–60 GB). Concurrent same-key returns 409, not silence. Failure mode: Redis down: fail closed vs DB unique-constraint fallback — pick one and defend it. Redis + DB primary failing together → fail closed, reject new payments rather than write unprotected.
Fraud Service — synchronous money path
A separate gating step before authorization: synchronous fraud score inside the latency budget. Calling out fraud/risk scoring as its own step with its own latency budget is a rubric nice-to-have. Scope cut: place the service, skip its ML internals. Failure mode: Post-capture fraud checks mean clawing money back.
Token Vault — synchronous money path
Tokenizes card data at the ingress and provides card tokens for PSP calls. Raw card data never enters your systems; everything downstream stores tokens. Frame PCI DSS as boundary minimization, not compliance recital. Network segmentation keeps the cardholder data environment isolated. Failure mode: “Encrypt the card numbers and store them” — encrypted PANs are still cardholder data: full PCI scope (HSMs, segmentation, audits) across your stack.
PSP Gateway — synchronous money path
Owns PSP calls: timeouts, circuit breakers, multi-PSP routing. The PSP call carries its own idempotency key derived from the client’s. A major PSP outage (e.g., Stripe’s 2019 outage, cited by the engine) is the canonical availability probe — the breaker opens and traffic routes to the secondary PSP. Failure mode: Failover raises the split-PSP reconciliation question.
External PSPs — external
Perform card authorization and hold card data. Delegating card auth and card storage to a PSP is correct and defensible. The PSP can’t join 2PC — hence the saga with compensating actions (auth→void, capture→refund, ledger→reversing entry). Card auths expire in ~7 days. Failure mode: “The PSP handles idempotency” — Stripe dedupes calls to Stripe, not your ledger entries, state transitions, or webhooks.
PostgreSQL — durable state
ACID store for the payments table, the append-only double-entry ledger, and outbox events — written in one transaction. ACID is a constraint, not a preference — DB choice is driven by ACID, not throughput. Synchronous replication on money tables; RPO for money is zero. Failure mode: Async replication with 2s lag loses ~2,300 committed payments on failover.
Ledger Service — durable state
Every movement is ≥1 debit + 1 credit summing to zero. No UPDATE, no DELETE; corrections are reversing entries. Balances are derived, never source-of-truth. Distinct from the event log, which records ALL state changes including non-financial ones — conflating the two is a listed misconception. Failure mode: Hot merchant: 10K TPS against one account row collapses on lock contention. Fix: sub-account sharding + batched writes; balance reads sum sub-accounts (cached).
Outbox Relay — async shell
Relays outbox rows to Kafka. The event row is written in the same ACID transaction as the state change. This is what makes events trustworthy: no event exists for a transaction that never committed. Failure mode: “Publish to Kafka then write to DB” — ghost events for transactions that never committed.
Kafka — async shell
Async fan-out to webhook delivery and reconciliation feeds. Partition by payment_id + sequence numbers + idempotent consumers. Failure mode: Kafka guarantees per-partition ordering only — random keys reorder a payment’s events.
Webhook Delivery — async shell
Typed events (payment.authorized / captured / refunded …), HMAC-signed, at-least-once with exponential backoff + DLQ. Receiver dedupes by event ID; no ordering promise — the payload carries a sequence number. Failure mode: Fire-and-forget: ~5% first-attempt failure means merchants silently missing payment confirmations. Unsigned webhooks invite spoofed “payment.succeeded”.
Reconciliation — async shell
Nightly three-way match: internal ledger vs PSP settlement file vs bank statement. Not optional — it is how you prove the ledger is correct. Mismatch categories: matched / missing-ours / missing-theirs / amount drift. Automation by category (currency rounding, timezone cutoffs, T+1 timing); human escalation for the residue. Failure mode: It doubles as the detection layer for anything that slips past idempotency. $500K unaccounted for: bucket mismatches, check settlement cutoff timing first, replay ledger events for the residual.

Whiteboard minimum

A passing diagram has all of these.

  • Client / Merchant SDK
  • Load balancer or API gateway
  • API gateway with rate limiting
  • Payment processing service
  • Primary data store (transactions + ledger)
  • Message queue (Kafka/SQS) for async processing
  • Payment Service Provider integration (Stripe/Adyen)
  • Double-entry ledger service

Staff+ additions

Unprompted.

  • Fraud detection service
  • Reconciliation / settlement service
  • Webhook / notification delivery service
  • PCI-compliant token vault (HSM-backed)
  • Monitoring / alerting / audit trail

Required flows

  • Client must hit gateway/LB, never direct to services
  • Gateway routes to payment service
  • Payment service writes to DB and ledger
  • Payment service communicates with PSP
  • Payment service publishes events to queue
  • Consumers process events from queue
  • Payment service invokes fraud check before authorization
  • Token vault provides card tokens for PSP calls

Forbidden flows — penalized on sight

  • Client must never access DB directly
  • Client must not call PSP directly — bypasses fraud and ledger
  • Gateway should not bypass payment service to query DB
  • Notification service should not call PSP
  • Client must never write ledger directly

Critical paths

  • Payment processing path must go through gateway and service (client → gateway → payment service → PSP)
  • Every payment must create ledger entries (payment service → ledger → database)

The request path, narrated — this is your whiteboard script

  1. The gateway authenticates the API key and applies per-merchant rate limits.
  2. The payment service — stateless — takes the atomic idempotency claim. Nothing else happens until the claim is won.
  3. A synchronous fraud score runs inside the latency budget — post-capture fraud checks mean clawing money back.
  4. The PSP call goes through the PSP gateway: timeouts, circuit breakers, multi-PSP routing. A major PSP outage (Stripe, 2019 — cited by the engine) is the canonical availability probe.
  5. On response, ONE ACID transaction writes the state transition + ledger entries + outbox event.
  6. The outbox relay publishes to Kafka; webhook delivery and reconciliation fan out from there — off the money path.

The database is PostgreSQL-class ACID — a constraint, not a preference — with synchronous replication on money tables. Async replication with 2s lag loses ~2,300 committed payments on failover; RPO for money is zero.

Checkpoint 06

Everything hangs off one commit: state + ledger + outbox in a single ACID transaction. Async begins only after it succeeds.

07 · Deep dives · minutes 30–45

The four that decide the interview

1 — Double-charge prevention, end to end

The probe

“Your merchant times out and retries. Walk me through why the customer isn’t charged twice.”

Your answer, in order

  1. The claim is atomic — Redis SETNX or a DB unique constraint. Read-then-write has a TOCTOU race two concurrent retries will find; that’s why “just check first” fails.
  2. The PSP call carries its own idempotency key, derived from the client’s — so a retry can’t re-authorize either.
  3. A concurrent request on the same key gets a 409 — never silence, never a second charge.
  4. Reconciliation doubles as the detection layer for anything that slips through.

Staff extra — Redis-down policy: fail closed, or fall back to the DB unique constraint. Pick one and defend it. Scope keys per merchant so cross-tenant collisions are impossible.

2 — Saga failure modes

The probe

“The PSP authorized, your ledger write failed — and the compensating void is now rate-limited. What happens?”

Your answer, in order

  1. Name the pattern and why: the PSP can’t join a two-phase commit, so the flow is a saga with compensating actions — auth→void, capture→refund, ledger→reversing entry.
  2. Saga state is persisted — recovery survives orchestrator crashes.
  3. A recovery worker retries the compensation with backoff; a stuck-saga sweeper escalates on TTL.
  4. The damage is bounded: an orphaned auth expires with the ~7-day card-auth window — no money moved.

Staff extra — orchestration over choreography for money: one component must own saga state, or nobody does.

3 — Hot merchant account

The probe

“One merchant is doing 10K TPS and ledger latency just blew past the SLA. Fix it.”

Your answer, in order

  1. Diagnose out loud before fixing: every entry contends for one account row — this is lock contention, not capacity. More connections and bigger hardware change nothing.
  2. Shard the merchant account into sub-accounts so writes spread across rows.
  3. Batch the ledger writes.
  4. Balance reads sum the sub-accounts, cached.

Staff extra — the diagnosis is most of the credit; say “contention, not capacity” before naming any fix.

4 — Reconciliation

The probe

“Nightly reconciliation just found 2,000 mismatches — $500K unaccounted for. Go.”

Your answer, in order

  1. Don’t audit by hand — bucket first: matched / missing-ours / missing-theirs / amount drift.
  2. Check settlement cutoff timing before anything else — T+1 timing, timezone cutoffs, and currency rounding explain most mismatches.
  3. Replay ledger events for the residual — the append-only ledger is what makes replay possible at all.
  4. Automate resolution by category; humans get only what automation can’t classify.

Staff extra — the match is three-way — internal ledger vs PSP settlement file vs bank statement — and a disagreement is a finding to investigate, never a merge conflict to overwrite.

Scenario player

The same system under attack. What breaks, what the naive design does wrong, what the correct mechanism does.

critical

What breaks

The PSP call succeeded, but the response was lost — the merchant sees a timeout and retries with the same idempotency key.

What the naive design does

“Just check if the payment already exists.” Merchant+amount matching is ambiguous (legitimate duplicate purchases) and check-then-create has a race window.

The correct mechanism

  1. The retry arrives with the same Idempotency-Key; the atomic claim finds the key already taken.
  2. The stored original response is replayed to the merchant.
  3. No second PSP call is made — the customer is charged exactly once.
  4. Reconciliation doubles as the detection layer for anything that slips through.

Components involved

Client / Merchant SDKAPI GatewayPayment ServiceIdempotency StorePSP Gateway

Rapid-fire

  • Shard by merchant_id, never date-only (permanent hot shard).
  • Multi-currency = minor units + currency code + FX-rate snapshot at transaction time; someone owns the auth→settlement FX drift.
  • Correlated failure (Redis + DB primary together) → fail closed, reject new payments rather than write unprotected.
  • Load testing via PSP sandbox + traffic replay, never real cards.
Checkpoint 07

Every deep dive is the same move: name the seam, give the mechanism, bound the damage.

08 · Traps & misconceptions

Ten claims that fail this interview

Each claim below is wrong. Read the claim, decide why it fails — then open it. These mirror the misconception patterns the simulator probes.

01 “payment is a single synchronous step”

Why it fails — Auth, capture, and settlement fail independently on different timescales; no answer for “charged but response lost.”

Instead — Model the state machine and what triggers each transition; auth-only + later capture is a feature (hotel holds), not an edge case.

02 “just check if payment already exists”

Why it fails — Merchant+amount matching is ambiguous (legitimate duplicate purchases) and check-then-create has a race window.

Instead — Client idempotency key checked atomically, original response stored and replayed.

03 “the PSP handles idempotency”

Why it fails — Stripe dedupes calls to Stripe, not your ledger entries, state transitions, or webhooks.

Instead — End-to-end idempotency: API-edge dedup + idempotent PSP call + outbox-atomic ledger/event writes.

04 “eventual consistency is fine for payments”

Why it fails — Temporary disagreement about money is a live financial bug (double charge / unpaid shipment).

Instead — ACID on the money path; eventual only for reads around it; say where the line is.

05 “refund just deletes the payment record”

Why it fails — Destroys the audit trail; financial regulation requires ~7-year records including reversals.

Instead — Refund = new transaction with opposite ledger entries, linked to the original, own state machine.

06 “encrypt the card numbers and store them”

Why it fails — Encrypted PANs are still cardholder data: full PCI scope (HSMs, segmentation, audits) across your stack.

Instead — Tokenize at ingress; isolated HSM-backed vault; everything else stores tokens.

07 “update the balance column directly”

Why it fails — Corrupts under concurrency, no audit trail, named red flag in the rubric.

Instead — Append-only double-entry ledger; balances derived / cached-with-verification; corrections via reversing entries.

08 “fire-and-forget webhooks”

Why it fails — ~5% first-attempt failure = merchants silently missing payment confirmations.

Instead — At-least-once + backoff + DLQ, HMAC signatures, event-ID dedup, no ordering promise.

09 “publish to Kafka then write to DB”

Why it fails — Ghost events for transactions that never committed.

Instead — Transactional outbox: event row in the same ACID txn, relayed via CDC/poller.

10 “Kafka guarantees global ordering”

Why it fails — Only per-partition; random keys reorder a payment’s events.

Instead — Partition by payment_id + sequence numbers + idempotent consumers.

Failure drills — would you catch it?

Six attack scenarios from the engine, tagged by severity. Commit to an answer before opening the mechanism.

critical A merchant’s payment request times out. They retry with the same amount but forgot the idempotency key. Customer is charged twice.

The mechanism — Idempotency keys, required and documented — a retried request must never charge twice. Without the key, merchant+amount matching is ambiguous; reconciliation is the detection layer for what slips through.

critical PSP authorizes the payment, but the ledger write fails. The compensating void also fails because the PSP is now rate-limiting you.

The mechanism — Persisted saga state surviving orchestrator crashes, retried compensations with backoff, a stuck-saga sweeper with TTL escalation, orphaned auths bounded by ~7-day card-auth expiry.

high A major merchant processes 10K TPS. All ledger entries contend for the same merchant account row. Lock wait times exceed your latency SLA.

The mechanism — Sub-account sharding + batched writes; balance reads sum sub-accounts (cached). This is lock contention, not capacity — more connections don’t help.

critical Your primary PSP (Stripe) experiences a 2-hour outage during Black Friday peak. In-flight payments are stuck.

The mechanism — Circuit breaker opens; route to the secondary PSP through the PSP gateway; expect the split-PSP reconciliation question.

high Nightly reconciliation finds 2,000 mismatches — your ledger shows more transactions than the PSP settlement file. $500K is unaccounted for.

The mechanism — Bucket mismatches by category (matched / missing-ours / missing-theirs / amount drift), check settlement cutoff timing first, replay ledger events for the residual.

critical Redis idempotency store crashes and database primary failover occurs simultaneously. You lose dedup protection during a period of potential duplicate writes.

The mechanism — Fail closed — reject new payments rather than write unprotected. Correlated failure is the case your Redis-down policy must already have decided.

09 · How this gets scored

The rubric, verbatim

Scored as: “Staff+ engineer at a payments / fintech infrastructure team.”

Must-haves

  • Treats idempotency as a first-class concern: client-supplied idempotency keys with 24h TTL, stored in Redis or DB, replay returns the original response.
  • Designs a double-entry ledger (every transaction = 2 entries: debit + credit) and explains why a single ‘balance’ table is wrong.
  • Names the payment state machine explicitly: CREATED → PROCESSING → AUTHORIZED → CAPTURED → SETTLED, and what triggers each transition.
  • Sizes throughput (~1,160 TPS avg, ~3,500 TPS peak), ledger growth (~200M rows/day), 7-year retention (~256 TB).
  • Addresses PCI DSS scope: tokenization at the ingress, raw card data never stored in our systems, all storage references tokens.

Nice-to-haves

  • Discusses webhooks for async settlement / state transitions with retries, signature validation, and dedup at the receiver.
  • Proposes a reconciliation pipeline (nightly batch matching internal ledger vs PSP settlement files) and explains why it’s not optional.
  • Calls out fraud / risk scoring as a separate gating step before authorization, with its own latency budget.
  • Addresses chargeback / refund flow as a state-machine extension, not a special case.

Red flags

  • Updating a single ‘account_balance’ column instead of using a ledger — corrupts under any concurrent write or partial failure.
  • No idempotency story — client retries result in duplicate charges, every time.
  • Storing raw card numbers anywhere in the application — PCI DSS violation, $1M+/yr compliance cost increase.
  • Treating the PSP (Stripe / Adyen) as ‘just call this API’ with no retry / state reconciliation — guarantees stuck transactions.

The verdict ladder

Strong hire Drives ledger + idempotency + state machine, sizes throughput + retention, addresses PCI + reconciliation unprompted.
Hire Picks ledger + idempotency correctly, addresses state machine with light prompting, sizes the system.
Lean hire Names ledger but shallow on idempotency OR reconciliation OR PCI scope.
Lean no-hire Missing the idempotency story OR proposes balance-column over ledger OR no PCI awareness.
No hire Single balance column design, OR no retry / idempotency, OR raw card storage.
Strong no-hire Fundamental misunderstanding — treats this as generic CRUD with money as a number, not a transactional contract.

Depth-ladder explorer

Six topics, four levels each — verbatim from the depth rubric. Slide L1→L4 and watch the same answer upgrade.

L1L4L2

probed in: requirements, high-level design, deep dive

L1 — surface

Mentions preventing duplicate payments or retries without a concrete mechanism

L2

Proposes client-generated idempotency keys with server-side dedup lookup before processing

L3

Designs the idempotency store (key → outcome mapping with TTL), discusses TOCTOU races on concurrent retries, and covers end-to-end idempotency across PSP + ledger + webhook

L4 — staff+

Quantifies the idempotency store size (100M keys × 256 bytes ≈ 25 GB), addresses key collision probability, cache vs DB tradeoff for lookup latency, and idempotency key scoping per merchant to prevent cross-tenant collisions

This question rewards engineers who treat money as a contract, not data. A Strong Hire designs the ledger, names idempotency mechanism, walks the state machine, and addresses PCI scope unprompted. A Lean Hire picks the components but is shallow on idempotency or reconciliation. A No Hire reaches for a balance column. Weight reliability_thinking and engineering_judgment most heavily — every shortcut here costs real money or a regulatory finding.

— grading guidance, payment_system.judge_rubric.yaml

10 · FAQ

Common questions

How hard is the payment system interview question?
One of the harder standard questions — not because of scale (3,500 peak TPS is modest) but because the failure mode is silent correctness loss: a race between two retries, a ledger that doesn’t balance, an event published for a transaction that never committed. Interviewers grade imprecision here as lost money.
What should I cover in the first ten minutes?
Requirements only — the four that gate everything: the multi-step payment flow, idempotency stated as “a retried request must never charge twice,” explicit scale numbers, strong-vs-eventual consistency boundaries. Jumping to Kafka in minute two reads as a red flag.
What is the single most common mistake?
A mutable balance column updated on every payment — it corrupts under concurrent writes, loses the audit trail, and can’t be reconciled. Second: no idempotency story at all.
Do I need to know PCI DSS in detail?
No — you need the scope argument: raw card numbers never enter your systems; tokenization at the ingress; everything downstream stores tokens. The trap is “encrypt cards in our database” — encrypted cardholder data still drags the whole system into PCI scope.
Can I just say “Stripe handles that”?
For card auth and card storage, yes — delegating to a PSP is correct and defensible. But the PSP doesn’t run your ledger, idempotency layer, webhook delivery, or reconciliation. “The PSP handles idempotency” is a listed misconception in the grading rubric.
How should I budget 45 minutes?
Roughly: 8 min requirements + scope cuts, 5 estimation, 7 entities + API, 10 high-level design, 15 deep dives — the deep dives are where the hire/no-hire separation happens.

You’ve read the answer. Could you defend it?

Every candidate who fails this question has read a page like this one. The interview isn’t recall — it’s 45 minutes of follow-ups against your specific design, live. The simulator asks this exact question, probes the same traps listed above, and scores you on the same rubric — then tells you, honestly, where you actually stand.

Free 15-min session · No card · AI interviewer, honest verdict