1. Prompt
After a user submits an order, inventory, payment, delivery, and coupon services must complete fulfillment. Any step can fail due to a business rule or network fault, and a committed remote transaction cannot be rolled back. Design a Saga that ends in CONFIRMED or CANCELLED, explaining each local transaction and compensation.
2. Constraints and clarifications
- Each service runs a local ACID transaction only against its own database.
- Eventual consistency is acceptable, but inventory and payment holds cannot remain forever.
- Retries can execute a step more than once; services must protect state with idempotency keys.
- Distinguish retryable technical failures, non-retryable business rejections, and compensation failures needing a human.
3. Core approach
A Saga decomposes a long transaction into ordered local transactions T1 ... Tn. Each successful step records progress and triggers the next; if a later step fails, completed steps run compensations Ck ... C1 in reverse order. A compensation is a new business operation, such as releasing inventory, voiding a payment authorization, cancelling delivery, or returning a coupon, rather than a database rollback.
An orchestrated Saga has a durable coordinator that stores state and the next action, which suits explicit workflow, timeout, and human-intervention requirements. Event-driven choreography removes the central coordinator but makes visibility and cycle control harder. In either style, carry a saga_id, step_id, version, and idempotency key on every command and event.
4. Reference implementation
start(order):
saga = create_saga(order.id, state="RESERVE_STOCK")
dispatch(saga, "ReserveStock")
on_step_result(saga_id, step_id, result):
saga = load_and_lock(saga_id)
require result.version == saga.version + 1
if result.success:
saga.completed_steps.append(step_id)
saga.version += 1
next = next_step(saga)
persist(saga)
dispatch(next) if next else finish_confirmed(saga)
else if result.business_rejection:
saga.state = "COMPENSATING"
persist(saga)
dispatch(compensation_for_last_completed(saga))
else:
schedule_retry_or_timeout(saga, step_id)
on_compensation_result(saga_id, step_id, result):
record_attempt(saga_id, step_id, result)
if result.success:
dispatch(previous_compensation(saga))
else:
mark_manual_intervention(saga, reason=result.error)5. Consistency and correctness
Coordinator state must be durable or a crash can lose the next action. Every command and event uses an idempotency key; the consumer records a processed step_id before committing its business change, preventing retries from reserving inventory twice. There is also a send gap between completing a step and publishing the next command, so an outbox, reliable queue, or CDC is needed for eventual visibility.
Compensation usually runs in reverse success order, but not every action has a strict inverse. Some effects need a business adjustment, such as a refund instead of retracting a notification already sent. Status queries should expose the current step, completed steps, retry count, and human-intervention reason rather than reporting “processing” as success.
6. Follow-ups and traps
- Do not describe Saga as atomic rollback across databases; it provides recoverable eventual consistency.
- Compensation can fail too, so add retries, dead letters, alerts, and human takeover instead of an infinite automatic loop.
- A global distributed lock expands the failure domain and cannot undo a remote business commit.
- Define TTLs for inventory and payment holds; release them with a timer or event when the Saga expires.
7. Further reading
Compare orchestration with choreography: orchestration centralizes state, order, and timeouts, while choreography decouples services through events but makes end-to-end tracing harder. Discuss non-compensatable side effects, semantic locks, version conflicts, audit logs, and when a single database transaction is simpler.
8. Interview scoring points
Can decompose local transactions
The candidate should list inventory, payment, delivery, and coupon steps and state that each service commits only its own local transaction.
Can design a compensation state machine
They should show success, failure, and reverse compensation paths, distinguishing business rejection, technical retry, and human takeover.
Can handle idempotency and reliable messaging
They should use saga_id, step_id, idempotency keys, and an outbox or reliable queue to cover coordinator crashes and command-send gaps.
Can state business boundaries
They should acknowledge that compensation is not rollback and discuss TTLs, non-compensatable side effects, status queries, and the user experience of eventual consistency.