Free preview

Why this matters: in this problem, the code that matters is small and specific: the confirm flow that catches a timeout and lands in UNKNOWN instead of FAILED, the create that replays instead of duplicating, and the reconciler that settles what's hanging. Interviewers watch for exactly those paths — and for ledger appends happening at settling moments, not scattered wherever. This walkthrough writes them in live order, narration included.

Vocabulary types first

java
enum PaymentState { CREATED, CONFIRMING, SUCCEEDED, FAILED, UNKNOWN } record Money(long minorUnits, String currency) {} // cents, never floats record LedgerEntry(String intentId, EntryType type, Money amount, Instant at) {} enum EntryType { CHARGE_SUCCEEDED, REFUND_ISSUED, CORRECTION } class PaymentIntent { final String id; final String idempotencyKey; // client-supplied, fixed at creation final Money amount; PaymentState state = PaymentState.CREATED; Money refunded = Money.zero(); // tracks the refundable remainder }

Two narration beats while typing: money is integer minor units, because floating-point cents is how you end up explaining a missing penny to finance; and the idempotency key is final on the intent — set once at creation, it never changes, which is the whole point.

The gateway port, with the method everyone forgets

java
interface PaymentGateway { // Throws GatewayTimeout when the outcome is genuinely unknown. ChargeResult charge(String idemKey, Money amount, Duration timeout) throws GatewayTimeout; // The reconciler's tool: what happened to this key, really? Optional<ChargeResult> lookup(String idemKey); }

Say it as you write lookup: "this is the method that makes UNKNOWN survivable — without a way to ask the gateway what happened, unknown would be a dead end." In tests, a stub implementation flips between success, decline, and thrown timeouts on command; that stub is all this problem needs — no HTTP client, no webhook plumbing.

Create: replay, don't duplicate

java
class PaymentService { Map<String, PaymentIntent> intents; // intentId -> intent Map<String, String> byIdemKey; // idemKey -> intentId List<LedgerEntry> ledger; // append-only, ever PaymentGateway gateway; PaymentIntent createIntent(Money amount, String idemKey) { String existing = byIdemKey.get(idemKey); if (existing != null) return intents.get(existing); // replay: same intent back PaymentIntent intent = new PaymentIntent(newId(), idemKey, amount); intents.put(intent.id, intent); byIdemKey.put(idemKey, intent.id); return intent; }

The double-click test: two identical create calls, one intent. The second call returns the first's result — no error, no duplicate. Idempotency means replays are boring.

Confirm: the timeout lands in UNKNOWN

java
PaymentState confirm(String intentId) { PaymentIntent intent = intents.get(intentId); if (intent.state == PaymentState.SUCCEEDED || intent.state == PaymentState.UNKNOWN) return intent.state; // replay-safe: change nothing intent.state = PaymentState.CONFIRMING; try { // The SAME key rides every attempt — the gateway dedups on it, // so a retried confirm cannot bill twice. ChargeResult r = gateway.charge(intent.idempotencyKey, intent.amount, Duration.ofSeconds(30)); settle(intent, r); } catch (GatewayTimeout t) { intent.state = PaymentState.UNKNOWN; // NOT FAILED. We don't know. } return intent.state; } private void settle(PaymentIntent intent, ChargeResult r) { if (r.approved()) { intent.state = PaymentState.SUCCEEDED; ledger.add(new LedgerEntry(intent.id, EntryType.CHARGE_SUCCEEDED, intent.amount, now())); } else { intent.state = PaymentState.FAILED; // gateway SAID no } }

Point at the catch block and hold the room there for a second: "this is the whole problem. The timeout path sets UNKNOWN — failed is reserved for the gateway actually declining." Then point at settle: the ledger append lives inside the settling function, so success-and-entry happen together or not at all — there is no code path that reaches SUCCEEDED without writing the ledger, and none that writes the ledger without settling.

The reconciler: the exit from UNKNOWN

java
void reconcile() { for (PaymentIntent intent : intentsIn(PaymentState.UNKNOWN)) { Optional<ChargeResult> r = gateway.lookup(intent.idempotencyKey); if (r.isPresent()) settle(intent, r.get()); // same settle: state + ledger // gateway still can't say? stay UNKNOWN — never guess. } }

Small loop, load-bearing lines. It reuses settle, so reconciled payments hit the exact same state-plus-ledger discipline as prompt ones. And the no-answer case stays unknown — the system tolerates prolonged ignorance and never converts it to a guess.

Refunds: validate against the remainder

java
void refund(String intentId, Money amount) { PaymentIntent intent = intents.get(intentId); require(intent.state == PaymentState.SUCCEEDED, "only settled payments refund"); require(amount.lte(intent.amount.minus(intent.refunded)), "exceeds refundable remainder"); // own gateway call, own small lifecycle; on success: intent.refunded = intent.refunded.plus(amount); ledger.add(new LedgerEntry(intent.id, EntryType.REFUND_ISSUED, amount, now())); }

The remainder check is the line to narrate: three partial refunds of 20 against a 50 payment — the third must bounce. History stays honest because each refund is its own ledger entry against the original; nothing about the charge entry changes.

What you'd say about complexity

Every operation is hash-map work plus one gateway call — O(1) locally, and the gateway's 30-second worst case towers over everything, which is why confirm must never loop on it synchronously. The reconciler is O(u) in unknown payments per sweep; at thousands of payments a day with rare timeouts, u is small — but say the honest bit: its cost is bounded by gateway calls, so it batches or paces itself rather than hammering a gateway that's already unhealthy.

Key takeaway

Write it in this order: states and money types, the gateway port with both charge and lookup, create that replays on a seen key, confirm whose timeout catch sets UNKNOWN — never FAILED — and a reconciler that settles through the same settle() as the happy path, so ledger appends happen at settling moments and nowhere else. The three lines interviewers are watching for: the replay return in create, the UNKNOWN assignment in the catch block, and the remainder check in refund.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue