Một khách hàng đặt đơn trên sàn thương mại điện tử. Hệ thống cần làm ba việc: tạo order ở order service, trừ tiền ở payment service, và giảm tồn kho ở inventory service. Ba service, ba database riêng, ba local transaction riêng. Payment thành công, inventory cũng trừ xong, nhưng order service crash giữa chừng, đơn hàng không tồn tại trong DB nhưng tiền đã trừ và hàng đã giữ. Khách hàng bị trừ tiền mà không có đơn, kho thì kẹt số lượng.

Nếu ba service dùng chung một database, một transaction ACID giải quyết gọn: BEGIN, ba câu INSERT/UPDATE, COMMIT hoặc ROLLBACK. Nhưng microservices tách database là nguyên tắc cốt lõi, mỗi service sở hữu data riêng, không share schema. Vậy làm sao đảm bảo tính nhất quán xuyên service khi không có transaction chung?

Câu trả lời truyền thống là Two-Phase Commit (2PC). Câu trả lời thực dụng cho hệ thống phân tán hiện đại là saga pattern, chia distributed transaction thành chuỗi local transaction, mỗi bước có compensating action để “hoàn tác” khi bước sau thất bại. Bài này đi sâu vào hai biến thể của saga, orchestration và choreography, cùng những bài toán thực tế mà ai triển khai saga cũng phải đối mặt: timeout, trạng thái treo, idempotency, và saga log.


Vì sao không dùng 2PC

Two-Phase Commit nghe rất hấp dẫn trên lý thuyết: một coordinator gửi PREPARE cho tất cả participant, tất cả vote YES, coordinator gửi COMMIT, xong. Atomicity đảm bảo, hoặc tất cả commit hoặc tất cả rollback.

Nhưng trên production với hệ thống phân tán, 2PC có ba vấn đề nghiêm trọng mà khiến nó hiếm khi là lựa chọn đúng.

Thứ nhất, blocking. Khi participant đã vote YES trong phase 1 nhưng chưa nhận COMMIT/ROLLBACK từ coordinator, nó phải giữ lock trên resource cho đến khi coordinator phản hồi. Nếu coordinator crash sau khi nhận đủ vote nhưng trước khi gửi quyết định, tất cả participant bị treo vô thời hạn, lock giữ, request mới bị block, throughput rơi về zero. Đây không phải edge case lý thuyết: network partition giữa coordinator và participant xảy ra thường xuyên hơn người ta nghĩ.

Thứ hai, single point of failure. Coordinator là thành phần không thể thiếu, mất coordinator là mất khả năng hoàn thành hoặc huỷ transaction. Dù có thể replicate coordinator, việc phục hồi trạng thái transaction đang dở giữa chừng rất phức tạp, đặc biệt khi coordinator crash giữa phase 2 và một số participant đã commit trong khi số khác chưa.

Thứ ba, hệ sinh thái không hỗ trợ. Hầu hết message broker hiện đại (Kafka, RabbitMQ, NATS) không implement XA protocol. Nhiều database NoSQL (MongoDB trước 4.0, Cassandra, DynamoDB) không hỗ trợ distributed transaction. Ngay cả khi tất cả thành phần hỗ trợ XA, latency của 2PC tăng tuyến tính theo số participant, với 5 service, mỗi network round-trip 10ms, 2PC đã thêm ít nhất 100ms vào mỗi transaction. Ở quy mô hàng nghìn transaction/giây, overhead này không chấp nhận được.

2PC phù hợp cho hệ thống đơn giản, ít participant, cùng vendor database, và yêu cầu strong consistency tuyệt đối. Với microservices phân tán trên nhiều tech stack, saga là giải pháp thực dụng hơn, chấp nhận eventual consistency để đổi lấy availability và scalability.


Saga là gì

Hiểu nôm na: saga giống như đặt cọc nhiều nơi khi tổ chức tiệc, đặt cọc khách sạn, nhà hàng, MC lần lượt. Nếu một bên không được, bạn gọi huỷ các bên còn lại. Không cần tất cả cùng ký một hợp đồng chung; mỗi bên xử lý riêng, nhưng phải có sẵn kịch bản huỷ cho từng bên.

Saga pattern được Hector Garcia-Molina và Kenneth Salem đề xuất năm 1987 trong bối cảnh long-lived transaction trên một database duy nhất. Ý tưởng cốt lõi: thay vì một transaction lớn chạy lâu giữ lock, chia thành chuỗi transaction nhỏ (local transaction), mỗi transaction commit ngay khi hoàn thành. Nếu transaction thứ N thất bại, chạy compensating transaction theo thứ tự ngược từ N-1 về 1 để hoàn tác những gì đã làm.

Áp dụng vào distributed systems, mỗi local transaction chạy trên một service riêng, commit vào database riêng. Không có lock xuyên service, không có coordinator kiểu 2PC. Mỗi bước hoàn thành thì báo hiệu cho bước tiếp theo. Nếu một bước thất bại, chuỗi compensating action chạy ngược lại.

Lấy ví dụ đặt hàng:

sequenceDiagram participant Client participant Order participant Payment participant Inventory ````text Client->>Order: Tạo đơn (PENDING) Order->>Payment: Trừ tiền Payment-->>Order: Thành công Order->>Inventory: Giữ hàng Inventory-->>Order: Thành công Order->>Order: Cập nhật CONFIRMED Order-->>Client: Đơn hàng thành công ```text

Nếu inventory service báo hết hàng:

sequenceDiagram participant Order participant Payment participant Inventory ```text Order->>Payment: Trừ tiền Payment-->>Order: Thành công Order->>Inventory: Giữ hàng Inventory-->>Order: Thất bại (hết hàng) Order->>Payment: Hoàn tiền (compensate) Payment-->>Order: Hoàn tiền OK Order->>Order: Cập nhật CANCELLED ```text

Saga đảm bảo: hoặc tất cả bước hoàn thành (forward recovery), hoặc tất cả bước đã hoàn thành được compensate (backward recovery). Không có trạng thái nửa vời, dù có thể có khoảng thời gian hệ thống ở trạng thái không nhất quán (tiền đã trừ nhưng đơn chưa confirm). Đây là bản chất của eventual consistency mà team cần chấp nhận khi dùng saga.

Một điểm quan trọng cần hiểu rõ: compensating action không phải undo theo nghĩa database rollback. Nó là một transaction mới, có semantic riêng. Hoàn tiền không phải “xoá record trừ tiền” mà là “tạo record hoàn tiền mới”, khách nhận notification “đã hoàn tiền”, bank statement có dòng refund riêng. Compensating action có thể phức tạp hơn action gốc rất nhiều.


Orchestration, bộ điều phối trung tâm

Trong mô hình orchestration, một saga orchestrator (hay saga coordinator) điều khiển toàn bộ flow. Orchestrator biết thứ tự các bước, gọi từng service theo sequence, nhận kết quả, và quyết định bước tiếp theo hoặc compensate.

flowchart TD O[Saga Orchestrator] -->|1. Tạo order| OS[Order Service] OS -->|OK| O O -->|2. Trừ tiền| PS[Payment Service] PS -->|OK| O O -->|3. Giữ hàng| IS[Inventory Service] IS -->|OK| O O -->|4. Confirm order| OS IS -->|FAIL| O O -->|Compensate: hoàn tiền| PS O -->|Compensate: huỷ order| OS

Orchestrator hoạt động như một state machine, tại mỗi trạng thái, nó biết chính xác phải gọi service nào và xử lý response thế nào. Trạng thái saga được persist vào database (saga log) để phục hồi khi orchestrator crash.

Ưu điểm của orchestration

Toàn bộ business flow nằm ở một nơi duy nhất, orchestrator. Khi cần hiểu “đặt hàng đi qua những bước nào”, đọc code orchestrator là đủ. Không cần trace event qua 5 service để ghép lại flow. Đây là ưu điểm lớn nhất cho team cần onboard engineer mới hoặc debug production.

Orchestrator dễ thêm logic phức tạp: conditional branching (nếu đơn hàng quốc tế thì thêm bước customs check), parallel execution (gọi payment và inventory song song nếu không phụ thuộc nhau), retry policy cho từng bước. Những thứ này trong choreography phải encode vào event schema và consumer logic, phân tán khắp nơi.

Monitoring và alerting cũng đơn giản hơn: orchestrator biết saga đang ở bước nào, bước nào đang chậm, bao nhiêu saga đang chờ response. Dashboard saga state distribution cho biết ngay health của flow, 100 saga stuck ở bước payment là dấu hiệu payment service có vấn đề.

Nhược điểm của orchestration

Orchestrator là single point of coupling, mọi service đều phụ thuộc vào nó, ít nhất là về mặt giao tiếp. Nếu orchestrator chậm, toàn bộ flow chậm. Nếu orchestrator crash và không có HA, tất cả saga đang chạy bị treo. Dù có thể replicate orchestrator, nó vẫn là thành phần trung tâm mà team phải vận hành và monitor riêng.

Orchestrator dễ trở thành god service nếu không cẩn thận. Bắt đầu với orchestrate đặt hàng, rồi thêm orchestrate hoàn hàng, rồi orchestrate đổi hàng, rồi orchestrate xử lý dispute, cuối cùng orchestrator chứa business logic của cả hệ thống, trở thành monolith mới. Nguyên tắc: orchestrator chỉ biết thứ tự bướcchính sách retry/compensate, không chứa business logic, logic nằm trong từng service.

Thêm service mới vào flow yêu cầu sửa orchestrator, coupling ở deployment level. Nếu team inventory muốn thêm bước warehouse allocation, team đó phải coordinate với team sở hữu orchestrator để deploy thay đổi cùng lúc, hoặc thiết kế orchestrator đủ dynamic để cấu hình flow mà không đổi code.


Choreography, sự kiện phân tán

Trong mô hình choreography, không có orchestrator trung tâm. Mỗi service publish event sau khi hoàn thành local transaction, các service khác subscribe event đó và phản ứng tương ứng. Flow được “lập trình” bằng cách kết nối event giữa các service.

flowchart LR OS[Order Service] -->|OrderCreated| EB[(Event Bus)] EB -->|OrderCreated| PS[Payment Service] PS -->|PaymentCompleted| EB EB -->|PaymentCompleted| IS[Inventory Service] IS -->|InventoryReserved| EB EB -->|InventoryReserved| OS OS -->|OrderConfirmed| EB ```text IS -->|InventoryFailed| EB EB -->|InventoryFailed| PS PS -->|PaymentRefunded| EB EB -->|PaymentRefunded| OS OS -->|OrderCancelled| EB ```text

Order service tạo đơn, publish OrderCreated. Payment service nghe event đó, trừ tiền, publish PaymentCompleted. Inventory service nghe PaymentCompleted, giữ hàng, publish InventoryReserved. Order service nghe InventoryReserved, confirm đơn.

Nếu inventory thất bại: publish InventoryFailed. Payment service nghe InventoryFailed, hoàn tiền, publish PaymentRefunded. Order service nghe PaymentRefunded, huỷ đơn.

Ưu điểm của choreography

Mỗi service hoàn toàn tự chủ, nó chỉ biết event nào cần nghe và event nào cần publish. Thêm service mới vào flow không cần sửa service hiện có, service mới chỉ cần subscribe event phù hợp. Ví dụ: thêm notification service gửi email khi OrderConfirmed, không service nào khác biết hoặc cần biết notification service tồn tại.

Không có single point of failure ở tầng application, mỗi service chạy độc lập. Nếu payment service chậm, chỉ saga liên quan payment bị ảnh hưởng, không block flow khác. Dĩ nhiên event bus (Kafka, RabbitMQ) trở thành dependency chung, nhưng message broker thường được thiết kế HA sẵn.

Choreography phù hợp với tổ chức mà mỗi team sở hữu service riêng và deploy độc lập. Team payment không cần coordinate với team order khi thay đổi internal logic, miễn event contract không đổi.

Nhược điểm của choreography

Flow bị phân tán, không có chỗ nào nhìn toàn bộ saga từ đầu đến cuối. Để hiểu “đặt hàng đi qua những bước nào”, phải mở code của từng service, xem nó subscribe event gì và publish event gì, rồi ghép lại trong đầu. Với 5 service đã phức tạp, với 10 service là nightmare khi debug production.

Cyclic dependency dễ xảy ra khi flow phức tạp. Service A nghe event của B, B nghe event của C, C nghe event của A, tạo vòng lặp sự kiện khó phát hiện. Mỗi service thêm event mới mà không ai review tổng thể, một ngày event storm xảy ra: một event trigger chuỗi phản ứng vòng lặp, message queue đầy, hệ thống overload.

Compensating flow trong choreography khó implement đúng hơn orchestration. Trong orchestration, orchestrator biết chính xác bước nào đã hoàn thành và cần compensate ngược lại. Trong choreography, mỗi service phải tự biết “khi nào cần compensate” dựa trên event nhận được, logic này dễ sai khi có nhiều nhánh conditional. Ví dụ: payment service nhận InventoryFailed thì hoàn tiền, nhưng nếu payment chưa hoàn thành lúc InventoryFailed đến thì sao? Race condition giữa event rất phổ biến trong choreography.


Compensating action, không phải undo đơn giản

Đây là phần mà nhiều team đánh giá thấp khi thiết kế saga. Compensating action cho mỗi bước cần được thiết kế cẩn thận, nó là một business operation hoàn chỉnh, không phải DELETE đối ứng với INSERT.

Ví dụ thực tế

Payment service trừ 500,000 VND thành công. Saga cần compensate. Compensation không phải “xoá record payment”, nó là tạo refund transaction mới. Refund có ID riêng, timestamp riêng, có thể cần gọi payment gateway để hoàn tiền thật (Stripe refund, bank transfer ngược). Khách nhận notification “Đơn hàng đã huỷ, tiền sẽ hoàn trong 3-5 ngày làm việc”. Refund có thể thất bại (gateway timeout, bank reject), lúc đó compensation cũng cần retry logic riêng.

Inventory service giảm tồn kho 2 đơn vị. Compensation là tăng tồn kho 2 đơn vị. Nghe đơn giản, nhưng nếu trong khoảng thời gian giữa reserve và compensate, có khách khác đã mua hàng và tồn kho đã về 0 thì sao? Compensation vẫn phải cộng lại 2 đơn vị, nhưng logic xung quanh (recalculate available stock, notify backorder queue) cần chạy đúng.

Notification service đã gửi email xác nhận đơn hàng. Compensation? Không thể “thu hồi” email đã gửi. Compensation ở đây là gửi email khác thông báo huỷ đơn. Nếu hai email đến cùng lúc, khách thấy “Đơn hàng đã xác nhận” rồi ngay sau đó “Đơn hàng đã huỷ”, UX tệ nhưng về mặt dữ liệu thì đúng. Đây là trade-off của eventual consistency mà product team cần biết.

Semantic reversal, không phải technical undo

Mỗi compensating action phải là semantic reversal, đảo ngược ý nghĩa nghiệp vụ, không phải đảo ngược thao tác kỹ thuật. Tạo order → compensate bằng huỷ order (set status CANCELLED, không phải DELETE row). Trừ tiền → compensate bằng hoàn tiền (tạo refund record, không phải update balance ngược). Reserve inventory → compensate bằng release reservation (cộng lại số lượng available).

Không xoá data khi compensate, giữ lại toàn bộ history để audit và debug. Record gốc (payment đã trừ) vẫn tồn tại với status phù hợp, record compensation (refund) tạo mới. Đây là nguyên tắc không chỉ đúng về kỹ thuật mà còn bắt buộc về compliance, financial transaction không bao giờ được xoá.

Pivot transaction và không compensate được

Trong một saga, có thể có bước mà sau khi hoàn thành thì không thể compensate. Gọi đây là pivot transaction. Ví dụ: service gọi API bên thứ ba để ship hàng, khi hàng đã giao cho đơn vị vận chuyển, không thể “thu hồi” tự động. Hoặc service gửi SMS/push notification, tin nhắn đã gửi thì không lấy lại được.

Thiết kế saga phải đặt pivot transaction càng cuối càng tốt trong chuỗi. Tất cả bước có thể compensate nên chạy trước. Khi đã qua pivot, saga chỉ có thể forward, retry bước còn lại cho đến khi thành công, không có đường lùi. Đây là lý do nhiều saga e-commerce đặt bước “ship hàng” cuối cùng, sau khi order confirmed, payment settled, inventory reserved.


Timeout và trạng thái treo

Trong hệ thống phân tán, không phải mọi request đều trả response. Network partition, service crash, pod bị OOM kill, bước nào đó trong saga có thể “mất tích” mà không trả thành công hay thất bại. Đây là trạng thái treo, saga không biết tiến hay lùi.

Saga state machine

Mỗi saga nên được model như state machine rõ ràng. Trạng thái chuyển đổi dựa trên response từ service hoặc timeout:

stateDiagram-v2 [*] --> OrderCreated OrderCreated --> PaymentPending: gọi payment PaymentPending --> PaymentCompleted: payment OK PaymentPending --> PaymentFailed: payment fail PaymentPending --> PaymentTimeout: timeout PaymentCompleted --> InventoryPending: gọi inventory InventoryPending --> InventoryReserved: inventory OK InventoryPending --> InventoryFailed: inventory fail InventoryPending --> InventoryTimeout: timeout InventoryReserved --> Confirmed: confirm order PaymentFailed --> Cancelled: huỷ order InventoryFailed --> Compensating: hoàn tiền Compensating --> Cancelled: compensate xong PaymentTimeout --> NeedInvestigation: chuyển manual queue InventoryTimeout --> NeedInvestigation: chuyển manual queue

Mỗi bước có timeout rõ ràng. Nếu payment service không phản hồi trong 30 giây, saga không nên chờ mãi. Nhưng xử lý timeout phức tạp hơn xử lý failure, vì bạn không biết bước đó đã hoàn thành hay chưa. Payment service có thể đã trừ tiền thành công nhưng response bị mất trên đường về. Nếu saga coi timeout là failure và chạy compensate, có thể compensate cho thứ chưa xảy ra (hoàn tiền cho payment chưa trừ) hoặc bỏ sót thứ đã xảy ra (không hoàn tiền cho payment đã trừ).

Xử lý timeout

Ba chiến lược phổ biến cho timeout:

Retry rồi mới fail. Khi bước timeout, retry 2-3 lần với exponential backoff. Nếu vẫn không response, coi như failure và chạy compensate. Chiến lược này đơn giản nhưng giả định rằng retry an toàn, tức bước đó phải idempotent (sẽ nói ở phần sau). Nếu retry tạo duplicate payment thì tệ hơn không retry.

Query trạng thái. Trước khi compensate, hỏi service đó “payment XYZ đã thành công chưa?”, dùng saga ID hoặc idempotency key để query. Nếu thành công thì tiến tiếp, nếu chưa thì compensate. Chiến lược này chính xác hơn nhưng yêu cầu mỗi service expose API query trạng thái, không phải service nào cũng có.

Chuyển sang manual queue. Khi timeout và không thể tự quyết định, saga chuyển sang trạng thái NeedInvestigation và đẩy vào dead letter queue hoặc manual intervention queue. Operator hoặc support team review, kiểm tra trạng thái thực tế trên từng service, và resolve thủ công. Nghe “không elegant” nhưng đây là chiến lược an toàn nhất cho trường hợp ambiguous, tốt hơn nhiều so với tự động quyết định sai.

Trên thực tế, production system thường kết hợp cả ba: retry trước, query trạng thái nếu retry vẫn timeout, chuyển manual nếu vẫn ambiguous. Percentage rất nhỏ saga cần manual intervention, nhưng 0.01% của 1 triệu transaction/ngày là 100 case cần người xem, đủ để cần tool và quy trình cho việc này.


Saga log và phục hồi sau crash

Orchestrator (hoặc từng service trong choreography) phải persist trạng thái saga vào storage bền vững. Nếu orchestrator crash giữa saga, khi restart nó cần biết saga đang ở bước nào để tiếp tục hoặc compensate, không phải bắt đầu lại từ đầu.

Saga log là gì

Saga log là bảng (hoặc collection) ghi lại mỗi bước chuyển trạng thái của saga. Cấu trúc tối thiểu:

saga_id  | step        | status    | payload           | created_at
---------+-------------+-----------+-------------------+--------------------
abc-123  | CREATE_ORDER| COMPLETED | {order_id: 456}   | 2026-04-27 09:00:01
abc-123  | PAYMENT     | COMPLETED | {payment_id: 789} | 2026-04-27 09:00:02
abc-123  | INVENTORY   | PENDING   | {sku: "X", qty: 2}| 2026-04-27 09:00:03

```text
Khi orchestrator restart, nó query saga log: saga `abc-123` đang ở bước INVENTORY với status PENDING → tiếp tục retry bước inventory hoặc đợi response.

Saga log phải được ghi **trước khi gọi service**, write-ahead pattern. Sequence là: ghi log "INVENTORY PENDING" → gọi inventory service → nhận response → ghi log "INVENTORY COMPLETED" hoặc "INVENTORY FAILED". Nếu crash giữa bước gọi và nhận response, khi restart saga thấy PENDING → biết cần retry hoặc query trạng thái.

Nếu crash trước khi ghi log thì không sao, saga chưa bắt đầu bước đó, retry từ bước trước. Nếu crash sau khi ghi log nhưng trước khi gọi service thì khi restart, saga sẽ gọi service, service nhận request lần đầu, không có vấn đề (miễn idempotent). Window rủi ro duy nhất là crash sau khi service xử lý nhưng trước khi ghi log response, lúc đó restart sẽ gọi service lần nữa, và lần gọi phải idempotent.

### Local transaction cùng saga log

Đây là pattern quan trọng: **business action và saga log update phải nằm trong cùng local transaction**. Order service tạo order và ghi saga log trong cùng một transaction trên database của order service. Nếu dùng database khác cho saga log, bạn lại rơi vào bài toán distributed transaction giữa business DB và saga log DB, đệ quy vấn đề.

Trong orchestration, saga log thường nằm trên database của orchestrator service. Trong choreography, mỗi service giữ saga log local, phức tạp hơn vì saga state phân tán trên nhiều database.

### Phục hồi saga treo

Cron job hoặc background worker quét saga log định kỳ, tìm saga có bước PENDING quá lâu (dựa trên timeout configured). Saga treo được retry, query trạng thái, hoặc chuyển manual queue tuỳ policy. Job này cần chạy đủ thường xuyên, nếu quét mỗi 5 phút thì saga có thể treo tối đa 5 phút trước khi được phát hiện.

Cẩn thận với concurrent recovery: nếu có nhiều instance orchestrator và cả hai cùng pick up saga treo, cả hai sẽ retry cùng bước, race condition. Giải quyết bằng distributed lock (Redis SETNX, database advisory lock) hoặc partition saga theo ID range cho từng instance.

---

## Idempotency, yêu cầu bắt buộc

Saga chạy trong môi trường mà message có thể bị deliver nhiều lần (at-least-once delivery, chuẩn của hầu hết message broker), network timeout dẫn đến retry, và crash recovery dẫn đến replay bước đã chạy. Nếu bước nào đó không idempotent, retry sẽ gây side effect: trừ tiền hai lần, giảm tồn kho hai lần, gửi hai email confirm.

### Idempotency key

Mỗi bước trong saga phải có **idempotency key**, thường là saga ID + step name. Khi service nhận request với idempotency key đã thấy, nó trả response của lần xử lý đầu tiên mà không thực thi lại.

Payment service nhận request `{saga_id: "abc-123", action: "charge", amount: 500000}`. Lần đầu: trừ tiền, ghi record `(saga_id="abc-123", status="charged")`, trả OK. Lần hai (retry): check record, đã có `saga_id="abc-123"` với status "charged" → trả OK ngay mà không trừ tiền lần nữa.

Idempotency key phải lưu **trước hoặc cùng lúc** với side effect, nếu lưu sau thì crash giữa side effect và lưu key sẽ khiến retry tạo duplicate. Pattern chuẩn: trong cùng database transaction, INSERT idempotency record VÀ thực hiện business logic. Nếu transaction commit, cả hai đều có. Nếu crash trước commit, cả hai đều không có, retry an toàn.

### Compensating action cũng phải idempotent

Compensating action cũng bị retry, khi orchestrator crash sau khi gọi compensate nhưng trước khi ghi log compensate thành công, restart sẽ gọi compensate lần nữa. Nếu refund không idempotent, khách nhận refund hai lần.

Cùng pattern: idempotency key cho compensate dùng saga ID + step name + "compensate". Payment service nhận refund request cho `saga_id: "abc-123"`. Lần đầu: tạo refund record, hoàn tiền, trả OK. Lần hai: đã có refund record cho saga này → trả OK ngay.

---

## Orchestration vs choreography, khi nào dùng cái nào

Không có câu trả lời phổ quát, lựa chọn phụ thuộc vào đặc thù hệ thống và tổ chức.

### Orchestration phù hợp khi

Saga có **nhiều bước sequential** với logic conditional phức tạp, nếu payment fail thì compensate, nếu inventory partial thì chuyển sang backorder flow, nếu khách VIP thì skip fraud check. Encode logic này trong choreography rất rối và khó test.

Team cần **visibility** cao vào trạng thái saga, bao nhiêu đơn đang processing, bao nhiêu đang stuck, bước nào chậm nhất. Orchestrator cung cấp thông tin này tự nhiên vì nó theo dõi state machine centrally.

Tổ chức có ít team và deploy centralized, sửa orchestrator không phải bottleneck vì cùng team sở hữu.

Business domain yêu cầu **audit trail rõ ràng**, saga log tập trung ở orchestrator dễ query và report hơn saga log phân tán.

### Choreography phù hợp khi

Flow đơn giản, ít bước, ít conditional, tạo order → charge payment → reserve inventory → done. Choreography cho flow tuyến tính như vậy rất clean và dễ implement.

Tổ chức có **nhiều team autonomous** deploy độc lập, thêm service mới vào flow chỉ cần subscribe event, không cần coordinate deploy với team khác.

Hệ thống cần **loose coupling** tối đa, service không biết và không cần biết service nào khác tồn tại. Phù hợp khi flow có thể mở rộng bất kỳ lúc nào (thêm analytics service nghe event, thêm fraud detection service) mà không ảnh hưởng service hiện có.

Event-driven architecture đã sẵn có, nếu hệ thống đã có Kafka/event bus và team đã quen consume event, choreography là extension tự nhiên.

### Hybrid approach

Thực tế, nhiều hệ thống dùng kết hợp. Orchestrator điều phối flow chính (order → payment → inventory), nhưng mỗi bước completion publish event ra event bus cho các service phụ subscribe (notification, analytics, audit). Core flow dùng orchestration cho reliability và visibility, peripheral concerns dùng choreography cho flexibility.

---

## Anti-pattern thường gặp

### Saga không có compensating action

Đây là anti-pattern phổ biến nhất và nguy hiểm nhất. Team implement saga forward path (happy path) rồi "để sau" phần compensate. "Để sau" thường có nghĩa "không bao giờ", cho đến khi production incident xảy ra và tiền bị trừ mà đơn hàng không tồn tại, lúc đó fix trong áp lực incident. Mỗi bước trong saga PHẢI có compensating action được thiết kế, implement, và test trước khi deploy lên production.

### Saga step đồng bộ blocking

Orchestrator gọi service qua HTTP synchronous, chờ response, nếu service chậm 30 giây, thread orchestrator bị block 30 giây. 100 saga đồng thời gọi service chậm → thread pool cạn → orchestrator không xử lý saga mới → hệ thống nghẽn dây chuyền.

Saga step nên giao tiếp **bất đồng bộ** qua message queue: orchestrator gửi command message vào queue, service consume và xử lý, trả response qua reply queue. Orchestrator không block chờ, nó persist state "đang chờ payment response", rồi xử lý saga khác. Khi response đến, orchestrator resume saga từ state đã lưu.

Nếu phải dùng HTTP, set timeout ngắn (vài giây), dùng circuit breaker, và có fallback strategy (retry hoặc chuyển sang async).

### Ignoring timeout

Saga step không có timeout → service crash → saga chờ mãi → resource bị hold vĩnh viễn. Inventory đã reserve nhưng saga treo → hàng kẹt không bán được cho khách khác. Mỗi bước PHẢI có timeout, và timeout handler phải rõ ràng (retry, compensate, hoặc chuyển manual).

### Thiếu idempotency

Đã nói ở trên nhưng nhắc lại vì quá phổ biến. Không có idempotency, retry tạo duplicate, trừ tiền hai lần, reserve hàng hai lần, gửi email hai lần. Đây không phải edge case, retry xảy ra thường xuyên trong distributed system. Implement idempotency từ đầu rẻ hơn nhiều so với fix data inconsistency trên production.

### Saga quá dài

Saga có 15 bước, mỗi bước gọi một service, compensating flow dài 14 bước ngược. Probability thất bại tăng theo số bước, thời gian hoàn thành saga tăng, complexity compensation tăng, khả năng debug giảm. Nếu saga quá dài, consider chia thành **nhiều saga nhỏ** liên kết qua event, hoặc gom một số bước thành sub-flow trong một service.

---

## Framework và công cụ thực tế

### Temporal

Temporal (tiền thân là Cadence của Uber) là workflow engine phổ biến nhất cho saga orchestration hiện tại. Code workflow bằng ngôn ngữ lập trình quen thuộc (Go, Java, TypeScript, Python) thay vì DSL hay JSON config. Temporal xử lý sẵn persistence (saga log), retry, timeout, và recovery, developer chỉ cần viết business logic.

Ưu điểm lớn nhất của Temporal: code nhìn như sequential code bình thường, gọi payment, chờ kết quả, gọi inventory, chờ kết quả, nhưng underneath, Temporal persist state sau mỗi bước, crash recovery tự động, retry configurable. Developer không phải tự build state machine hay saga log.

Nhược điểm: thêm infrastructure component (Temporal server cluster), learning curve để hiểu execution model (deterministic replay), và debugging workflow cần tool riêng (Temporal UI). (Nói thật: phần lớn team adopt Temporal vì một kỹ sư đọc blog post của Uber/Stripe ca ngợi nó, không phải lý do tệ, nhưng nên pilot một saga nhỏ trước khi all-in vào workflow engine mới.) Với team nhỏ chưa có Temporal, adopt nó chỉ cho một saga có thể overkill.

### AWS Step Functions

Step Functions của AWS dùng JSON state machine definition (Amazon States Language). Tích hợp sẵn với Lambda, SQS, SNS, DynamoDB. Phù hợp cho hệ thống đã all-in AWS, không cần vận hành thêm infrastructure vì Step Functions là managed service.

Nhược điểm: vendor lock-in (state machine definition không portable), cost tăng nhanh với volume cao (tính phí theo state transition), và debug state machine JSON phức tạp hơn debug code bình thường.

### Custom implementation

Nhiều team build saga orchestrator tự viết, state machine đơn giản, saga log bảng trong Postgres, background worker quét saga treo. Approach này cho control hoàn toàn nhưng đòi hỏi team tự giải quyết mọi edge case: concurrent execution, crash recovery, idempotent retry, timeout management.

Nếu chọn custom, bắt đầu với state machine rõ ràng (enum trạng thái, transition table), saga log trong database cùng service, và recovery job chạy định kỳ. Đừng tự viết distributed lock hay message broker, dùng thứ có sẵn (Redis, Postgres advisory lock, Kafka/RabbitMQ).

Kinh nghiệm: custom implementation phù hợp khi saga đơn giản (3-5 bước, ít conditional), team đã có infrastructure message queue, và không muốn thêm dependency mới. Khi saga phức tạp lên hoặc có nhiều saga flow khác nhau, chi phí maintain custom solution tăng nhanh, lúc đó Temporal hay tương đương bắt đầu có ROI.

---

## Monitoring và observability cho saga

Saga xuyên nhiều service nên monitoring phải xuyên nhiều service tương ứng. Distributed tracing (OpenTelemetry) là bắt buộc, mỗi saga step là một span trong trace, trace ID truyền xuyên suốt từ bước đầu đến bước cuối (hoặc đến compensate cuối). Khi debug saga lỗi, mở trace ra thấy ngay bước nào chậm, bước nào fail.

Metric cần theo dõi: saga completion rate (bao nhiêu phần trăm saga hoàn thành thành công vs compensate vs stuck), saga duration P50/P95/P99 (saga chậm bất thường là dấu hiệu service downstream có vấn đề), compensate rate (tăng đột biến nghĩa là có bước đang fail nhiều), và stuck saga count (saga ở trạng thái PENDING quá lâu).

Alert nên đặt cho stuck saga count, saga treo là dấu hiệu rõ ràng nhất của vấn đề cần can thiệp. 10 saga stuck trong 5 phút có thể là service crash, 1000 saga stuck là incident cần page on-call.

Log mỗi state transition với saga ID, step name, status, và duration. Khi investigate incident, query log theo saga ID ra toàn bộ timeline, bước nào thành công, bước nào fail, compensate có chạy không, chạy bao lâu.

---

Saga là lựa chọn thực dụng thay cho 2PC khi service và database đã tách riêng, đổi strong consistency lấy availability, nhưng phải trả giá bằng việc thiết kế compensation cẩn thận ngay từ đầu, không phải sau khi incident xảy ra. Orchestration hay choreography là câu hỏi về cấu trúc team hơn là câu hỏi kỹ thuật, ai sở hữu flow này và cần visibility đến đâu?