Katkat

Implement Timeout Refund Workflow

DoorDashSoftware Engineer · Onsite

Last updated

Design and implement a stateful, idempotent refund workflow manager that safely handles gateway timeouts, retries, and duplicate requests.

Problem Statement

In food delivery platforms, orders that are not accepted by a restaurant within a specific timeout must be canceled and automatically refunded. External payment gateways are prone to network timeouts and transient failures. To prevent double refunds and stuck states, your workflow must enforce idempotent state transitions and retry-safety.

Implement the RefundWorkflowManager class to orchestrate order refund requests against a simulated payment gateway.

Class Interface

class GatewayResponse:
    SUCCESS = "SUCCESS"
    TIMEOUT = "TIMEOUT"
    ALREADY_PROCESSED = "ALREADY_PROCESSED"
    DECLINED = "DECLINED"

class OrderState:
    AWAITING_REFUND = "AWAITING_REFUND"
    REFUNDING = "REFUNDING"
    REFUNDED = "REFUNDED"
    MANUAL_REVIEW = "MANUAL_REVIEW"

class PaymentGateway:
    def execute_refund(self, idempotency_key: str, amount_cents: int) -> str:
        """
        Returns one of: GatewayResponse.SUCCESS, GatewayResponse.TIMEOUT,
        GatewayResponse.ALREADY_PROCESSED, GatewayResponse.DECLINED
        """
        pass

class RefundWorkflowManager:
    def __init__(self, gateway: PaymentGateway):
        pass

    def register_order(self, order_id: str, amount_cents: int) -> None:
        """Registers an order eligible for refund."""
        pass

    def process_refund(self, order_id: str, attempt_id: str) -> str:
        """
        Attempts to refund the order. Returns the resulting OrderState.
        """
        pass

    def get_order_state(self, order_id: str) -> str:
        """Returns the current state of the order."""
        pass

Workflow Rules

  1. Order Registration: Newly registered orders start in AWAITING_REFUND.
  2. State Transitions:
    • process_refund moves an AWAITING_REFUND order to REFUNDING and issues gateway.execute_refund with an idempotency key composed deterministically of order_id (e.g., refund_{order_id}). Note: attempt_id represents the caller's execution attempt and must not change the gateway idempotency key.
    • If the gateway returns SUCCESS or ALREADY_PROCESSED, the state transitions to REFUNDED.
    • If the gateway returns DECLINED, the state transitions to MANUAL_REVIEW.
    • If the gateway returns TIMEOUT (or raises an exception), the order remains in REFUNDING so subsequent retries can re-attempt with the exact same idempotency key.
  3. Idempotency & Re-entrancy:
    • If process_refund is called on an order that is already in REFUNDED or MANUAL_REVIEW, it should immediately return that terminal state without calling the gateway.
    • If process_refund is called on an order currently in REFUNDING (such as after a previous TIMEOUT), it should re-invoke gateway.execute_refund using the original idempotency key.
    • Calling process_refund on an unknown order_id should raise a KeyError.

Constraints

  • amount_cents > 0
  • order_id and attempt_id are non-empty strings of length <= 64.
  • All state transitions must be deterministic.

Example

# Setup
gateway = MockPaymentGateway() # First call returns TIMEOUT, second returns SUCCESS
manager = RefundWorkflowManager(gateway)
manager.register_order("order_123", 2500)

# Attempt 1: Gateway times out
state1 = manager.process_refund("order_123", "attempt_1")
# state1 -> "REFUNDING"

# Attempt 2: Retry with new attempt ID
state2 = manager.process_refund("order_123", "attempt_2")
# state2 -> "REFUNDED"

# Attempt 3: Duplicate invocation after completion
state3 = manager.process_refund("order_123", "attempt_3")
# state3 -> "REFUNDED" (Gateway not called again)

Hints

Hint 1

Tie the gateway's idempotency key directly to the order rather than the retry attempt so external calls remain strictly deduplicated.

Hint 2

Treat both SUCCESS and ALREADY_PROCESSED as successful completions that safely transition the order to REFUNDED.

Hint 3

Ensure terminal states (REFUNDED, MANUAL_REVIEW) short-circuit immediately without side effects.

Follow-up questions

  • How would you prevent race conditions if multiple workers call process_refund concurrently for the same order?
  • If a gateway timeout leaves an order in REFUNDING indefinitely, how would you implement an automated reconciliation job to query the gateway status?
  • How would you adapt this design to Temporal or Cadence activities and workflows?