Make a Courier Pay Workflow Resilient to Downstream Failures
DoorDashSoftware Engineer · Technical ScreenLast updated
Tests your ability to implement an idempotent, fault-tolerant workflow engine that safely executes financial transactions against an unreliable downstream ledger.
Problem Statement
In a courier platform, couriers must be paid accurately once deliveries are completed. You are tasked with implementing the core execution engine of a PayoutProcessor that processes payment instructions and posts them to an external, unreliable downstream ledger service.
The downstream ledger service has the following characteristics:
- It accepts an
idempotency_key,courier_id, andamount_cents. - It can return three types of responses:
SUCCESS: Payout was recorded.TRANSIENT_ERROR: Server busy or network timeout (unknown whether the ledger processed the request or not).TERMINAL_ERROR: Permanent failure (e.g., invalid courier account or closed ledger account). Retrying will never succeed.
- It exposes a
check_status(idempotency_key)endpoint that returnsSUCCESS,NOT_FOUND, orFAILED.
Your task is to implement the PayoutProcessor class with state persistence to guarantee:
- No Duplicate Payouts: A courier is never paid more than once for a given payout instruction.
- Durable Intent: Payout states are tracked (
PENDING,SUBMITTED,COMPLETED,FAILED_TERMINAL,REQUIRES_RECONCILIATION). - Ambiguous Timeout Handling: When a timeout or transient error occurs, the processor must probe the downstream service using
check_statusbefore attempting a retry or marking the job. - Bounded Retries: If downstream remains unreachable after a configurable
max_retries, mark the payout asREQUIRES_RECONCILIATIONso automated background reconciliation or an on-call engineer can inspect it safely.
Interface
from enum import Enum
from typing import Optional
class PayoutStatus(Enum):
PENDING = "PENDING"
COMPLETED = "COMPLETED"
FAILED_TERMINAL = "FAILED_TERMINAL"
REQUIRES_RECONCILIATION = "REQUIRES_RECONCILIATION"
class LedgerResponse(Enum):
SUCCESS = "SUCCESS"
TRANSIENT_ERROR = "TRANSIENT_ERROR"
TERMINAL_ERROR = "TERMINAL_ERROR"
class LedgerStatus(Enum):
SUCCESS = "SUCCESS"
NOT_FOUND = "NOT_FOUND"
FAILED = "FAILED"
class PayoutProcessor:
def __init__(self, ledger_client, store, max_retries: int = 3):
"""
ledger_client methods:
- post_payment(idempotency_key: str, courier_id: str, amount_cents: int) -> LedgerResponse
- check_status(idempotency_key: str) -> LedgerStatus
store methods:
- get_payout(payout_id: str) -> Optional[dict]
- save_payout(payout_id: str, data: dict) -> None
"""
pass
def process_payout(self, payout_id: str, courier_id: str, amount_cents: int) -> PayoutStatus:
"""
Executes the payout safely and returns the terminal or current state.
"""
pass
Constraints
amount_cents > 0payout_idis globally unique per delivery payout intent.- Downstream
idempotency_keymust be deterministically derived frompayout_id(e.g.,f"pay_{payout_id}"). store.save_payoutwrites synchronously to a persistent database.- The function should not loop indefinitely; retries must not exceed
max_retries.
Example
# Scenario: Transient network drop on first attempt, succeeded on check_status
# 1. store.save_payout(payout_id, status=PENDING)
# 2. ledger_client.post_payment(...) -> LedgerResponse.TRANSIENT_ERROR
# 3. ledger_client.check_status(...) -> LedgerStatus.SUCCESS
# 4. store.save_payout(payout_id, status=COMPLETED)
# Result: PayoutStatus.COMPLETED
Hints
Hint 1
Always persist the initial PENDING record before making any network calls to establish durable intent.
Hint 2
When encountering a TRANSIENT_ERROR during posting, immediately query `check_status` before blindly retrying to avoid race conditions or double charging.
Hint 3
If `check_status` returns NOT_FOUND after a transient error, it is safe to retry posting; if `check_status` itself fails or exhausts retries, transition to REQUIRES_RECONCILIATION.
Follow-up questions
- How would you handle a crash or power failure in the middle of `process_payout` right after `post_payment` succeeds but before `save_payout` completes?
- How does the transactional outbox pattern fit into this design if payments are triggered by delivery completion events in an event bus like Kafka?
- What metrics and alerting thresholds would you emit to distinguish between ledger service degradation and client-side misconfiguration?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.