Part 09 · Distributed Systems

Xử lý failure xuyên suốt Order–Inventory–Payment–Fulfillment

Không có một transaction ACID bao trùm nhiều database/service. Thiết kế đúng biến workflow thành durable state machine gồm local transactions, idempotent commands, reservations, compensation và reconciliation.

1. Trước tiên phân loại outcome của mỗi remote call

Outcome caller quan sátÝ nghĩa thậtHành động
Definite successNhận response hợp lệ; vẫn phải chống duplicate/replay.Persist transition tiếp theo.
Definite business failureVí dụ OUT_OF_STOCK/DECLINED; server đã trả quyết định terminal.Không retry mù; cancel/compensate.
Definite technical failure trước khi gửiKhông lấy được connection/DNS fail trước write, nếu client chứng minh được.Bounded retry hoặc fail/defer theo policy.
Unknown outcomeTimeout/reset sau khi request có thể đã tới và commit.Không suy ra failed; query bằng operation ID/idempotency key hoặc reconcile.

Trong production, nhiều lỗi “không gọi được” thực chất không đủ bằng chứng rằng Inventory chưa reserve. Retry chỉ an toàn khi command idempotent.

2. Full business path

Client
  -> Order: create PENDING
  -> Inventory: RESERVE stock with reservationId + TTL
  -> Payment: AUTHORIZE amount with paymentOperationId
  -> Order: CONFIRM
  -> Inventory: CONFIRM reservation (consume)
  -> Payment: CAPTURE authorization
  -> Fulfillment: CREATE shipment
  -> Order: COMPLETED

Failure path:
  RELEASE inventory / VOID authorization / REFUND capture
  + reconciliation + manual review when outcome is unknown

Tùy business có thể capture payment trước hoặc sau shipment. Quan trọng là định nghĩa point-of-no-return, compensation nào khả thi và customer-visible state ở mỗi giai đoạn.

3. Vì sao không trừ tồn kho ngay?

Dùng reservation thay vì cập nhật vĩnh viễn ngay từ đầu:

available = on_hand - active_reserved

RESERVE(orderId, sku, quantity, reservationId, expiresAt)
CONFIRM(reservationId)  // chuyển reserved thành consumed
RELEASE(reservationId)  // trả reserved về available
EXPIRE(reservationId)   // cleanup khi workflow chết/stuck

4. State machines theo từng owner

OwnerStates điển hình
OrderPENDING → INVENTORY_RESERVED → PAYMENT_AUTHORIZED → CONFIRMED → COMPLETED; failure sang CANCELLING/CANCELLED/MANUAL_REVIEW.
Inventory reservationACTIVE → CONFIRMED | RELEASED | EXPIRED.
PaymentCREATED → AUTHORIZING → AUTHORIZED → CAPTURED; hoặc DECLINED/VOIDED/REFUNDED/UNKNOWN.
FulfillmentREQUESTED → ACCEPTED → SHIPPED; shipment đã giao thường không thể rollback kỹ thuật.

Mỗi transition dùng compare-and-set/version, lưu reason, attempt, deadline và correlation/causation ID. Không cho event đến muộn làm state quay lùi.

5. Happy path bằng Saga orchestration

  1. Order Service transaction: tạo Order + Saga state + Outbox ReserveInventory.
  2. Inventory consume command: Inbox dedupe; reserve atomically; Outbox InventoryReserved.
  3. Orchestrator nhận result, persist state + Outbox AuthorizePayment.
  4. Payment idempotently authorize; emit success/decline/unknown.
  5. Success: confirm Order, rồi confirm reservation/capture theo business sequence.
  6. Gửi Fulfillment command; notification/analytics không nằm trên correctness path.

Mỗi “persist state + next command” phải cùng local transaction qua Outbox. Nếu process crash, relay tiếp tục sau restart.

6. Failure matrix xuyên toàn path

Điểm lỗiState/responseRecovery
Inventory definite unavailableOrder REJECTED_OUT_OF_STOCKKhông payment; trả business failure.
Inventory timeout/response lostINVENTORY_UNKNOWN hoặc tiếp tục pendingQuery reservationId; retry cùng ID; reconcile, không tạo ID mới.
Reserve thành công, Order crash trước ghi resultInventory ACTIVE, Saga chưa tiếnEvent redelivery/idempotent consume; TTL/reconciliation là safety net.
Payment declinedOrder CANCELLINGRelease inventory; sau release thành CANCELLED.
Payment timeout sau authorizationPAYMENT_UNKNOWNQuery provider by operation ID; không authorize lại với key mới.
Order confirm DB failure sau payment authorizedPayment AUTHORIZED, stock ACTIVERetry Order transition; nếu deadline hết thì void payment + release stock.
Inventory confirm thất bại sau Order confirmedInconsistent intermediate stateForward recovery retry confirm; nếu không thể, cancel/void/refund và manual review.
Capture success, response lostPayment UNKNOWNReconcile provider; không refund/recapture trước khi biết outcome.
Fulfillment unavailableOrder CONFIRMED, shipment pendingQueue/retry với deadline; không nhất thiết refund ngay nếu fulfillment phục hồi được.
Compensation RELEASE/VOID/REFUND failCOMPENSATION_PENDINGRetry idempotently, alert/backlog SLO, operator runbook.

7. Hai case ban đầu nằm ở đâu?

Case 1: Order gọi Inventory không thành công

Nếu chắc chắn request chưa được xử lý, bounded retry/circuit breaker hoặc trả 202 PENDING/503 theo API contract. Nếu timeout/reset sau khi gửi, đó là unknown outcome: dùng cùng reservation ID để retry/query. Circuit breaker bảo vệ capacity, không quyết định inventory có reserve hay chưa.

Case 2: Inventory đã trừ/reserve nhưng Order update fail

Không thể rollback transaction của Inventory từ Order Service. Lựa chọn ưu tiên là retry/forward recovery để Order bắt kịp result. Nếu workflow bị hủy hoặc quá deadline, gửi command ReleaseInventory. Reservation TTL và reconciliation sửa trường hợp event/compensation bị mất. Không dùng distributed lock hoặc gọi “API cộng lại 1” thiếu idempotency/state guard.

8. Sync hay async?

Thiết kếƯu điểmFailure cost
Sync REST chainResponse nhanh, code happy path dễ hiểuTemporal coupling, timeout/unknown outcome, retry amplification; vẫn cần durable recovery.
Async SagaAbsorb burst, durable commands, service outage không chặn request threadEventual UX, duplicates/order/schema, broker và operations.
HybridTạo Order trả 202, xử lý async; client poll/SSECần status resource và honest pending states.

Với booking cần phản hồi nhanh, có thể reserve sync rồi phần sau async, nhưng vẫn persist Saga state trước/giữa các side effect. Không có lựa chọn nào loại bỏ idempotency và reconciliation.

9. Retry, circuit breaker và bulkhead đặt ở đâu?

10. Idempotency và duplicate ở mọi command

ReserveInventory  key = reservationId
AuthorizePayment key = paymentOperationId
ConfirmOrder     key = sagaId + transitionVersion
ReleaseInventory key = reservationId
RefundPayment    key = originalPaymentId + refundPurpose

Consumer lưu Inbox/event ID hoặc unique business operation trong cùng transaction với effect. Cùng key khác payload phải conflict. Idempotency record cần scope, fingerprint, stored outcome và retention đủ dài hơn retry/replay window.

11. Ordering và stale events

Partition/key event theo aggregate khi cần order cục bộ; event có aggregate version. Consumer chỉ áp transition hợp lệ: PaymentAuthorized(v3) đến sau OrderCancelled(v4) không được confirm lại Order. Không dựa timestamp để tạo total order.

12. Reconciliation là thành phần bắt buộc

Job phải checkpoint, rate-limit, idempotent và xuất discrepancy report. Auto repair case chắc chắn; case tài chính/không đảo ngược đưa manual review với audit.

13. Client/API semantics

POST /orders -> 202 Accepted
Location: /orders/{orderId}

GET /orders/{orderId}
{
  "status": "PAYMENT_UNKNOWN",
  "retryAfter": 5,
  "failureCode": null
}

Client retry create bằng Idempotency-Key; server trả cùng Order. Không map mọi internal state ra ngoài, nhưng phải phân biệt processing, terminal failure và manual review. Webhook/SSE cũng phải tolerate duplicate/out-of-order.

14. Observability và SLO

15. Không nên làm

Giữ @Transactional của Order trong lúc gọi Inventory/Payment; nó không bao trùm DB khác và giữ connection/lock quá lâu.
Inventory “trừ 1” rồi khi lỗi “cộng 1” không có reservation ID, transition guard và idempotency.
Retry bằng operation ID mới, tạo duplicate reservation/payment.
Đánh dấu Order FAILED chỉ vì timeout, dù downstream có thể đã commit.
Gọi Saga là distributed rollback hoặc exactly-once.

16. Design checklist

Saga Pattern · Transactional Outbox · AWS Timeouts and Retries · Circuit Breaker