Payment System Design Interview Guide: Idempotency, Ledgering, and Reconciliation

Prepare payment system design with idempotency, unknown outcomes, balanced ledger entries, duplicate callbacks, and settlement reconciliation exercises.

Author: PracHub

Published: 10/1/2026

Payment System Design Interview Guide: Idempotency, Ledgering, and Reconciliation

October 1, 2026

Quick Overview

Retries must converge on one business operation while ledger and settlement remain separately verifiable.

Software EngineerFree

A payment request times out. The customer retries, the processor sends two callbacks, and yesterday's settlement report disagrees with your database. Payment system design interviews test whether those paths converge on one business operation with a verifiable financial record. An idempotency key, message queue, or database transaction is useful only when its exact boundary is clear.

Use PracHub's holds, captures, reconciliation, and idempotency exercise to practise the complete flow. These question records are preparation material, not predictions of a future interview. Provider behavior below comes from official documentation; the payment IDs, amounts, architecture, and accounting trace are original exercises, not candidate reports or financial advice.

A single pending payment receipt and a ledger beside a retry arrow

Define payment states before the service diagram

Ask whether the system authorizes a card, captures a previous authorization, refunds a charge, transfers wallet balances, or settles merchant funds. An authorization is not a captured payment, and a captured payment is not necessarily money already deposited in the merchant's bank account.

For an original baseline, process a merchant charge through one external provider, record successful captures, support refunds, and reconcile provider settlement. Keep each currency separate. Use integer minor units with an explicit currency scale; do not assume every currency has two decimal places.

Give the internal payment a stable ID and separate each external attempt. A payment can need more than one legitimate attempt after a definitive decline, while a retry of an unknown attempt must not become a new charge automatically. Those identities belong in the model before adding workers.

Proposed states include Created, Processing, Unknown, Succeeded, and Failed. Authorization, capture, refund, and settlement can have their own state machines rather than one overloaded “paid” flag. Declare which result lets the customer proceed and which result remains provisional.

Make idempotency a transactional contract

Idempotency means repeating the same logical operation has the same intended effect as applying it once. In a payment API, that requires a stable operation identity, parameters that cannot silently change, and durable records that survive retries and crashes. It does not mean the network delivers a request only once.

Scope a client key to the merchant and operation. Store a canonical request fingerprint, internal payment ID, status, and response. Enforce uniqueness in the database. Two concurrent arrivals for the same key must contend on the same record rather than both observing an absent cache entry and proceeding.

If a key is reused with a different amount or currency, return a conflict. A key that produced an accepted charge cannot later identify a refund. The application should distinguish retrying an operation from requesting another business action.

Official Stripe idempotency documentation describes replaying the stored result for a key, including some server errors, checking parameters, and permitting pruning after keys are at least 24 hours old. That provider contract is not a lifetime business deduplication guarantee. Keep your own durable identity and interpret retries according to the provider's current API behavior.

Walk through an unknown external outcome

In our original trace, merchant M1 submits key K7 for payment P17, a USD 100 charge. The API atomically creates the operation record and an outbox command. A worker uses a stable provider idempotency identity for the corresponding charge attempt.

The provider accepts the charge, but the worker loses the response. Internally, the outcome is Unknown. Marking it Failed and starting a new attempt with a new provider key can charge the customer again. The timeout proves that the caller lacks a result, not that the provider rejected the operation.

A recovery path queries the provider when supported or retries under the documented stable identity. It correlates the returned provider object with P17 and records the definitive result. If the provider's deduplication retention may have expired, recovery needs an explicit investigation policy rather than a blind retry that assumes the old key still protects it.

Return an operation resource that the client can poll, or supply a later notification. The customer-facing state should explain that confirmation is pending. Retain a support trail linking client key, payment ID, attempt ID, provider ID, and recovery decisions.

A payment request entering an unknown state and reconciling under the same identity

Separate atomic local writes from remote execution

An outbox solves a particular problem: the payment record and the instruction to dispatch work can commit in one local transaction. A dispatcher may deliver that instruction more than once. The consumer still needs deduplication and stable external identity.

No ordinary local transaction atomically commits your database and an unrelated processor. Draw the crash boundary after external success but before local persistence. The recovery procedure for that boundary is part of the design, not an exception to omit.

Lease a worker's claim if necessary, but do not treat lease expiry as proof that the old worker stopped. A stalled worker can resume while a new worker takes over. Protect local state transitions with versions or fencing, and protect the external effect with the provider's supported idempotency contract.

A callback can arrive before the worker records the provider response. Both paths should refer to the same provider object and compete to apply the same business transition. A database uniqueness constraint on the capture posting prevents two local journals for one recognized capture, even if two transport messages triggered it.

Write the ledger from one explicit perspective

A double-entry ledger records each journal transaction with balanced debits and credits. Accounts have types and a declared business perspective; a debit is not simply “money leaving,” and a credit is not simply “money arriving.” Official TigerBeetle accounting documentation explains these distinctions.

Use an original simplified payment-platform perspective. After a USD 100 capture, the platform has a USD 100 receivable from the processor and owes USD 100 to the merchant. Record debit Processor receivable 100 and credit Merchant payable 100. This teaching model omits taxes, reserves, FX, and contractual fee complexity.

Suppose the platform bears a USD 3 processor fee. Settlement pays USD 97 into its bank account: debit Bank cash 97, debit Fee expense 3, and credit Processor receivable 100. Debits and credits balance at each journal, while the receivable returns to zero.

The merchant payable remains 100 under that fee assumption. Paying the merchant debits Merchant payable 100 and credits Bank cash 100. The platform must fund the fee; receiving 97 does not magically satisfy a 100 liability. If the merchant bears the fee instead, the journals and contract differ. State the assumption rather than hiding it in a net amount.

Preserve invariants without rewriting history

A journal should have a stable business reference, currency, effective time, recording time, and immutable entries. Enforce balanced totals within each currency and atomic posting. Two entries with opposite signs in different currencies do not establish a balanced single-currency transaction.

Derive balances from entries or maintain a transactional projection that can be rebuilt and compared. A mutable account balance without its posting history is difficult to audit. A cache can accelerate balance reads, but it cannot be the only record of financial effects.

Corrections normally add reversal and replacement entries linked to the original transaction. Do not edit yesterday's successful capture into a refund and erase the evidence. A refund is a new business operation with its own identity, limits, provider outcome, and ledger effect.

Balance alone does not prove correctness. A duplicated balanced journal still overstates both sides; a posting to the wrong merchant can balance perfectly. Check uniqueness, account ownership, valid lifecycle transitions, and cumulative captured or refunded amounts as separate invariants.

Process callbacks as evidence, not ordered commands

Official Stripe webhook guidance says events may be delivered more than once and are not guaranteed to arrive in generation order. Verify signatures using the provider's required request representation, record delivery identity, and acknowledge only after the necessary durable intake step.

A durable inbox can separate quick receipt from slower processing. Deduplicate repeated event IDs, then protect the underlying business transition too: distinct events can concern the same payment object. A transport-level duplicate check is not enough to establish one capture journal.

Avoid “latest arrival wins.” An old Processing notification should not overwrite a confirmed Succeeded state. Apply permitted state transitions and, when needed, retrieve the authoritative provider object. Do not infer a total event order from timestamps that can tie or describe different objects.

Keep malformed or unrecognized events in an observable failure path. Repeatedly acknowledging without recording them can silently lose important evidence; endlessly retrying a permanent parse error can block useful work. State how operators diagnose and replay intake safely.

Reconcile at the correct grain and cutoff

Reconciliation compares independently obtained records to identify unexplained differences. Match the internal attempt or capture reference to the provider object and relevant settlement entries. A provider report row may represent a fee, refund, adjustment, or payout rather than an order.

Official Stripe balance transaction documentation identifies multiple balance-impacting transaction types. Our proposed reconciliation stores the source report, its cutoff, and match decisions before comparing currency-specific gross, fee, and net totals. It does not equate every movement with new revenue.

For P17, an internal capture of 100 and a bank deposit of 97 can reconcile when the report explains a fee of 3. Comparing only those two totals would create a false mismatch. Conversely, a missing capture reference is not resolved merely because the day's aggregate net happens to match.

Classify missing internal records, missing provider records, amount differences, unexpected duplicates, and timing differences separately. Track unresolved amount, age, and owner. Late settlement may be expected, but it needs a declared aging rule; “eventually consistent” is not a plan for indefinitely unexplained money.

Demonstrate failures before claiming exactly-once effects

PracHub practice questionWhat to practise
Design a Payment System: Holds and Captures, Batch Charges, Reconciliation, IdempotencyTrace holds, captures, and settlement.
Prevent Duplicate Payments Under High LoadTrace two concurrent requests for one payment.
Design an Idempotent Double-Entry LedgerWrite a balanced, idempotent journal.
Design Payment Processing with Idempotency and ReconciliationRecover an unknown provider result.
Design a Payment Lifecycle from Hold to Charge and SettlementExplain charge versus settlement transitions.

Build a replayable test history: two simultaneous client requests, timeout after provider acceptance, duplicate callback, callback before worker response, and settlement arriving the next day. The expected result is one recognized charge, one corresponding capture journal, and a traceable settlement difference until reconciliation completes.

Inject crashes at local commit, dispatch, external response, and journal posting boundaries. Verify restart from durable records, not from an in-memory flag. Test changed parameters under the same key, legitimate separate payments with equal amounts, and a refund that would exceed the captured amount.

Describe the achieved property precisely. You may guarantee one local posting per business reference while relying on a separate provider contract for external charging. A queue's delivery guarantee does not create an atomic financial effect across those boundaries.

Start the holds-and-captures payment design exercise by drawing the timeout-after-acceptance trace. Then write the capture and settlement journals from one perspective. If every retry, state change, and mismatch has a durable identity and an owner, your design is ready for meaningful follow-ups.

Sources and Further Reading

Primary references checked September 30, 2026. Accounting examples are simplified interview exercises; provider contracts and real accounting policies require their own implementation review.


Comments (0)