Debug a Payment Service's Charge Flow Against Retries, Crashes and Timeouts
Company: Mercor
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
You are asked to debug the charge flow of a payment service. The service charges a user for an order through an external payment provider and records the payment in its own database. The original code from the interview was not reported; the version below is a representative reconstruction of the flow under review.
```java
public class PaymentService {
private final PaymentProvider provider; // client for the external payment provider
private final PaymentRepository payments; // our database
public PaymentService(PaymentProvider provider, PaymentRepository payments) {
this.provider = provider;
this.payments = payments;
}
/** Charges the user for the order and records the result. */
public PaymentStatus pay(String orderId, String userId, long amountMinor) {
ChargeResult result = provider.charge(userId, amountMinor); // network call
payments.insert(new Payment(orderId, userId, amountMinor, result.status()));
return result.status();
}
}
```
Context for the reconstruction:
- `pay` is called through an HTTP endpoint, and its callers retry when a call fails or times out.
- `provider.charge` returns a result whose status is either succeeded or declined, and throws on network errors, timeouts and provider server errors.
- `amountMinor` is the amount in the currency's smallest unit, such as cents.
Find the ways this flow can charge a user twice or lose track of a payment, and restructure it so that it cannot.
### Clarifying Questions
- Does the provider accept an idempotency key on charge requests? If so, for how long does it remember a key?
- Can the provider be asked for the outcome of a charge by a reference we supply, and does it send webhooks?
- What identifies one logical payment from the caller's side: the order ID, or a request ID the client generates once per payment attempt?
- What should the caller be told while the outcome of a charge is still unknown?
### Part 1 — Failure scenarios
Walk through the concrete sequences of events in which the current code charges a user twice or leaves a charge with no record.
```hint Interrupt it everywhere
Consider a crash, a lost response or a timeout at each point in the flow, and what the caller does next in each case.
```
#### What This Part Should Cover
- Caller retries after a lost or slow response
- A crash or database failure between the provider call and the insert
- A provider call whose outcome is unknown
- Concurrent duplicate requests for the same payment
### Part 2 — A safe charge flow
Restructure the flow so that retries and duplicate requests can never produce a second charge, and a crash at any point leaves a record you can act on.
```hint Record intent before acting
Think about what your database should already contain at the moment the provider is called, and what key would let a repeated request recognize the first one.
```
#### What This Part Should Cover
- The deduplication mechanism, where its key comes from, and how the database enforces it
- The order of the database writes and the provider call, and the states a payment moves through
- Safe state transitions when several components may update the same payment
- What the caller receives in each case
### Part 3 — Outcomes you cannot know
The provider call times out. What state does the payment go into, and what eventually turns it into a final answer?
```hint Unknown is a state too
Rather than guessing success or failure inside the request, consider recording that the outcome is not yet known, and decide which component, using what information, can settle it later.
```
#### What This Part Should Cover
- Distinguishing definite failures from failures whose outcome is uncertain
- The process that resolves uncertain payments, and what it does for each outcome it finds
- Retrying safely, and escalation when the outcome stays unknown
- Monitoring for payments stuck in an undecided state
### What a Strong Answer Covers
- Every double-charge and lost-record scenario identified with a concrete sequence of events
- Deduplication enforced by the database, not only by an application-level check
- An intent record written before the external call, and an explicit payment state machine with guarded transitions
- Correct handling of ambiguous failures, with a reconciliation path and escalation
- The contract with the caller, observability, and the trade-offs of the chosen design
### Follow-up Questions
- The provider does not support idempotency keys at all. How do you avoid double charges when a charge must be retried?
- A provider webhook and your own background process try to finalize the same payment at the same moment. How do you keep the final state correct?
- How long must you keep idempotency keys, and what happens if a client retries after a key has been deleted?
- How would you make refunds safe against the same timeout uncertainty?
Overview: A debugging exercise on a payment service that charges users through an external provider and then records the result. You find how caller retries, crashes and timeouts can double-charge a user or lose a payment, then restructure the flow. It tests idempotency, payment state machines and reconciliation of unknown outcomes.
Read the full Mercor Software Engineer interview experience this question came from