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ật | Hành động |
|---|---|---|
| Definite success | Nhận response hợp lệ; vẫn phải chống duplicate/replay. | Persist transition tiếp theo. |
| Definite business failure | Ví dụ OUT_OF_STOCK/DECLINED; server đã trả quyết định terminal. | Không retry mù; cancel/compensate. |
| Definite technical failure trước khi gửi | Khô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 outcome | Timeout/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 unknownTù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/stuckreservationIdunique làm command retry-safe.- Atomic conditional update/row lock ngăn oversell.
- TTL tránh giữ stock vô hạn, nhưng expiry phải xét clock, long-running payment và race với confirm.
- Release/confirm cũng idempotent và kiểm tra transition hợp lệ.
4. State machines theo từng owner
| Owner | States điển hình |
|---|---|
| Order | PENDING → INVENTORY_RESERVED → PAYMENT_AUTHORIZED → CONFIRMED → COMPLETED; failure sang CANCELLING/CANCELLED/MANUAL_REVIEW. |
| Inventory reservation | ACTIVE → CONFIRMED | RELEASED | EXPIRED. |
| Payment | CREATED → AUTHORIZING → AUTHORIZED → CAPTURED; hoặc DECLINED/VOIDED/REFUNDED/UNKNOWN. |
| Fulfillment | REQUESTED → 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
- Order Service transaction: tạo Order + Saga state + Outbox
ReserveInventory. - Inventory consume command: Inbox dedupe; reserve atomically; Outbox
InventoryReserved. - Orchestrator nhận result, persist state + Outbox
AuthorizePayment. - Payment idempotently authorize; emit success/decline/unknown.
- Success: confirm Order, rồi confirm reservation/capture theo business sequence.
- 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ỗi | State/response | Recovery |
|---|---|---|
| Inventory definite unavailable | Order REJECTED_OUT_OF_STOCK | Không payment; trả business failure. |
| Inventory timeout/response lost | INVENTORY_UNKNOWN hoặc tiếp tục pending | Query reservationId; retry cùng ID; reconcile, không tạo ID mới. |
| Reserve thành công, Order crash trước ghi result | Inventory ACTIVE, Saga chưa tiến | Event redelivery/idempotent consume; TTL/reconciliation là safety net. |
| Payment declined | Order CANCELLING | Release inventory; sau release thành CANCELLED. |
| Payment timeout sau authorization | PAYMENT_UNKNOWN | Query provider by operation ID; không authorize lại với key mới. |
| Order confirm DB failure sau payment authorized | Payment AUTHORIZED, stock ACTIVE | Retry Order transition; nếu deadline hết thì void payment + release stock. |
| Inventory confirm thất bại sau Order confirmed | Inconsistent intermediate state | Forward recovery retry confirm; nếu không thể, cancel/void/refund và manual review. |
| Capture success, response lost | Payment UNKNOWN | Reconcile provider; không refund/recapture trước khi biết outcome. |
| Fulfillment unavailable | Order CONFIRMED, shipment pending | Queue/retry với deadline; không nhất thiết refund ngay nếu fulfillment phục hồi được. |
| Compensation RELEASE/VOID/REFUND fail | COMPENSATION_PENDING | Retry 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ểm | Failure cost |
|---|---|---|
| Sync REST chain | Response nhanh, code happy path dễ hiểu | Temporal coupling, timeout/unknown outcome, retry amplification; vẫn cần durable recovery. |
| Async Saga | Absorb burst, durable commands, service outage không chặn request thread | Eventual UX, duplicates/order/schema, broker và operations. |
| Hybrid | Tạo Order trả 202, xử lý async; client poll/SSE | Cầ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?
- Timeout/deadline cho mỗi hop và toàn workflow; retry chỉ transient + idempotent.
- Một retry owner, exponential backoff + jitter, max attempts/elapsed time; không retry mọi layer.
- Circuit breaker fail fast khi dependency suy yếu; fallback phải trả
PENDING/UNAVAILABLE, không success giả. - Bulkhead/concurrency limit theo Inventory/Payment; queue bounded và load shedding bảo vệ DB/pool.
- Business failure như OUT_OF_STOCK/PAYMENT_DECLINED không retry như technical failure.
10. Idempotency và duplicate ở mọi command
ReserveInventory key = reservationId
AuthorizePayment key = paymentOperationId
ConfirmOrder key = sagaId + transitionVersion
ReleaseInventory key = reservationId
RefundPayment key = originalPaymentId + refundPurposeConsumer 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
- Order pending quá SLA nhưng Inventory vẫn ACTIVE.
- Payment AUTHORIZED/CAPTURED nhưng Order không terminal.
- Order CONFIRMED nhưng thiếu ledger/inventory/fulfillment effect.
- Outbox quá lâu chưa publish, Inbox duplicate spike, DLQ tăng.
- Reservation sắp/đã expire khi Saga còn active.
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
- Business: success/decline/out-of-stock rate, end-to-end time-to-terminal.
- Stuck: age/count theo Saga state, compensation backlog, manual-review queue.
- Delivery: Outbox oldest age, consumer lag, duplicate count, DLQ/replay.
- Resources: thread/connection pools, DB locks, queue depth, dependency latency.
- Trace có orderId/sagaId/reservationId/paymentOperationId nhưng redact PII/secret.
15. Không nên làm
@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.16. Design checklist
- Mỗi service có data owner và local transaction rõ.
- Mỗi remote outcome được phân loại success/business failure/technical/unknown.
- Inventory dùng reserve/confirm/release/expire.
- Saga state và next command được persist durably.
- Mọi command/consumer/compensation idempotent.
- Event version/order và stale transition được kiểm soát.
- Compensation failure có retry, alert và operator path.
- Reconciliation cùng pending/manual-review UX tồn tại.
- Timeout/retry/circuit/bulkhead bảo vệ capacity.
- Failure injection chứng minh convergence và không double effect.