Staff / expert · Architecture and systems
The payment timed out, then succeeded late
A saga times out while charging a payment. Shipping is cancelled, then a late payment-success event arrives. What state should own recovery?
Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.
package main
import "fmt"
type State string
const (
Reserved State = "reserved"
Charged State = "charged"
Refunding State = "refunding"
Done State = "done"
)
func next(s State, event string) (State, error) {
switch {
case s == Reserved && event == "charged":
return Charged, nil
case s == Charged && event == "shipping-failed":
return Refunding, nil
case s == Refunding && event == "refund-confirmed":
return Done, nil
}
return s, fmt.Errorf("unexpected %s in %s", event, s)
}
func main() {
s, _ := next(Charged, "shipping-failed")
fmt.Println(s) // refunding, not done
s, err := next(s, "refund-confirmed")
fmt.Println(s, err)
}Reveal a strong answer
A reservation, payment authorization and shipment may belong to different services. They cannot usually share a database transaction. A saga records progress and issues commands whose effects can be retried or compensated. Compensation is another business operation, with its own failures, rather than a database rollback that erases everything that already happened.
Define a workflow ID, durable step state, command idempotency key, timeout policy and reconciliation owner. Separate orchestration from event choreography: a central coordinator makes recovery visible, while decentralized reactions need an equally explicit ownership story. Keep irreversible steps late where practical and plan manual resolution for states no automatic compensation can safely resolve. Shipping failure moves the workflow into refunding. It does not declare success because a refund command was merely sent. The second transition waits for confirmation. The pure transition function is easy to test, but a durable coordinator must persist the transition and the next command intent atomically.
A timeout is not proof that charging failed. Retrying with a new payment identity can double-charge. Compensation can be refused or delayed; do not mark the workflow completed before it is confirmed. Test duplicate commands, late success after timeout, coordinator restart and failure of the compensation itself. Use the smallest number of remote boundaries the business actually requires.
Common trap
A compensation command is another fallible business action, not an automatic rollback of time.
Try it
Define durable states for charge-pending, unknown outcome, refund-pending and confirmed completion, including duplicate and late events.
Worked solution
A timeout cannot be treated as a definite failure. Persist the stable payment identity and reconcile the provider’s result. A late success may require a policy-approved refund, but the workflow must remain refund-pending until confirmation. Persist transitions with command intent, make duplicate commands safe and define manual resolution when compensation cannot complete. A pure state transition can be unit-tested; recovery also needs restart and provider-contract tests.
Change the constraint
What changes if the compensation is itself delayed or an irreversible step already happened?
How did you do?
Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.
Not marked. Progress stays on this browser, separate from course completion.
Primary references
Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.