Debug an Async Payment Refund Function with Retries, Then Make It Idempotent
Company: Mercor
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
In a technical screen for an applied AI engineering role, you are handed the Python function below and asked to debug it. It is meant to take a stored refund request, issue the refund through the Stripe payment API with up to three attempts, and then record in the database that the refund is done.
```python
async def refund_money(request_id: str):
refund = await get_db_record(request_id)
if refund.status == "done":
return
for i in range(3):
try:
await stripe_api_call(
refund.request_id,
refund.amount
)
break
except Error:
continue
await mark_db_as_done(request_id)
```
Assume that `get_db_record` loads the refund request with the given ID, `stripe_api_call` asks the payment provider to refund `amount` for that request, and `mark_db_as_done` sets the record's status to `"done"`. Separately, the interviewer asked what idempotency is and why we need idempotent operations; this function is the natural place to answer that.
### Clarifying Questions
- Can this function run more than once for the same `request_id`, for example when a job is retried or a message is redelivered?
- What does `stripe_api_call` return, and which exceptions can it raise?
- What statuses can a refund record have besides `"done"`?
- When a refund cannot be completed, should the function raise to its caller, record a failure, alert someone, or all three?
### Part 1 — Find the bugs
List every defect or risk in this function. For each one, describe a concrete sequence of events that triggers it and what goes wrong for the customer or for the business.
```hint Follow every exit from the loop
For each way the loop can finish, ask what the database record says afterward and whether that matches what actually happened at the payment provider.
```
```hint Think beyond one uninterrupted call
The function reads state, calls an external system and then writes state. Consider what happens if that sequence is cut short partway through, or runs more than once.
```
#### What This Part Should Cover
- Each defect tied to a specific line and a concrete scenario that triggers it
- The consequence of each defect for money movement and for the stored state
- A distinction between definite bugs and risks that depend on how the helpers or the provider behave
- A ranking of the defects by severity
### Part 2 — Idempotency and a safe rewrite
Explain what idempotency is and why operations like this refund need to be idempotent. Then rewrite the function so that it is safe to retry and safe to run more than once for the same `request_id`. State what you need from the database and from the payment provider's API to make your version correct.
```hint Let the provider recognize a repeat
Think about what the refund call could carry so that the payment provider can tell a retry of an earlier request apart from a brand-new refund.
```
#### What This Part Should Cover
- A precise definition of idempotency, and why retries, timeouts and redelivery make it necessary for payments
- A rewrite that keeps the provider's side and the stored state consistent across retries, crashes and overlapping runs
- Which failures are retried, how, and which are not
- The final stored state for every outcome, including when every attempt fails
### What a Strong Answer Covers
- A systematic trace of the code's paths rather than a scan for surface problems
- Money correctness first: the customer is never refunded twice, and the system never records a refund that did not happen
- A clear view of which system holds the truth about a refund, and how the database and the provider are reconciled
- Production concerns: timeouts, backoff, logging, alerting, and how to test the fix
### Follow-up Questions
- The provider call times out. Did the refund happen, and how does your design find out?
- How would you test that the rewritten function never refunds twice, even with concurrent runs?
- Suppose the provider forgets idempotency keys after a day, and a stuck request is retried a week later. What happens, and how do you guard against it?
- How would this change if refunds were processed from a message queue with at-least-once delivery?
Overview: Find the defects in a short async Python function that issues a payment refund with up to three attempts and then marks the request done, then explain idempotency and rewrite the function so it is safe to retry and to run more than once. It tests failure-path reasoning, concurrency, error handling and correctness when money moves.