Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 23 — Compensation — undo logic

Business Process Mapping and BPMN Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đặt một tour du lịch trọn gói: vé máy bay, phòng khách sạn, và xe đưa đón. Hệ thống đã giữ vé máy bay thành công, giữ phòng khách sạn thành công, nhưng đến bước thuê xe thì nhà cung cấp hết xe. Bây giờ phải làm gì? Bạn không thể để khách hàng "mắc kẹt" với một nửa đơn hàng — đã có vé máy bay và phòng khách sạn nhưng không có xe để di chuyển. Hệ thống buộc phải hoàn tác (undo) cả hai bước đã làm trước đó: hủy giữ phòng, hủy giữ vé, và hoàn tiền nếu đã trừ.

Đây chính xác là vấn đề mà Compensation trong BPMN sinh ra để giải quyết. Trong các bài trước bạn đã học về Error Handling (Bài 22) — cách "bắt" lỗi và rẽ nhánh xử lý khi có sự cố. Nhưng Error Handling chỉ trả lời câu hỏi "khi lỗi xảy ra thì làm gì tiếp theo?". Còn Compensation trả lời một câu hỏi khó hơn nhiều: "những việc tôi đã hoàn thành thành công trước đó, giờ phải đảo ngược như thế nào?".

Sự khác biệt này cực kỳ quan trọng trong thực tế. Một activity đã chạy xong không tự nhiên "biến mất" được. Phòng khách sạn đã giữ thì phải có một hành động ngược lại để giải phóng nó. Tiền đã trừ thì phải có một giao dịch hoàn tiền. Đây không phải là rollback của database (vốn chỉ áp dụng cho một transaction kỹ thuật ngắn), mà là rollback ở cấp độ nghiệp vụ, kéo dài qua nhiều hệ thống, nhiều giờ, thậm chí nhiều ngày. Trong thế giới của microservices và quy trình phân tán, đây gọi là Saga pattern — và Compensation chính là cách BPMN biểu diễn Saga một cách trực quan. Nắm vững bài này, bạn sẽ thiết kế được những quy trình "an toàn" thực sự, không để lại trạng thái dở dang gây thất thoát tiền bạc và mất uy tín.

Khái niệm cốt lõi

Compensation là gì

Compensation (bù trừ / hoàn tác nghiệp vụ) là cơ chế trong BPMN cho phép định nghĩa logic "undo" cho một activity đã hoàn thành thành công. Điểm mấu chốt nằm ở cụm từ "đã hoàn thành thành công". Bạn không compensate một việc đang chạy hay chưa chạy — bạn chỉ compensate những việc đã xong xuôi nhưng giờ cần phải đảo ngược vì một lý do nào đó phát sinh sau này.

Ví dụ kinh điển: trong một quy trình đặt hàng có 5 bước tuần tự. Bước 1 đến bước 4 chạy ngon lành, nhưng bước 5 thất bại. Lúc này không thể đơn giản "dừng lại" được, vì 4 bước trước đã tạo ra những thay đổi thật trong hệ thống (đã trừ kho, đã trừ tiền, đã đặt vận chuyển). Compensation cho phép quy trình "đi lùi", gọi lần lượt logic undo của bước 4, bước 3, bước 2, bước 1 — đưa hệ thống về trạng thái như chưa từng có gì xảy ra.

Tại sao không dùng database transaction với ROLLBACK? Vì transaction của database chỉ tồn tại trong mili-giây đến vài giây, và chỉ trong phạm vi một cơ sở dữ liệu. Quy trình nghiệp vụ thì khác: bước "thanh toán" gọi sang cổng VNPay, bước "giữ kho" gọi sang hệ thống ERP, bước "đặt giao hàng" gọi sang Giao Hàng Nhanh. Mỗi bước là một transaction riêng đã được commit ở hệ thống bên ngoài. Bạn không thể ROLLBACK một giao dịch VNPay đã thành công — bạn chỉ có thể tạo một giao dịch hoàn tiền mới để bù lại. Đó chính là tinh thần của Compensation.

Hai thành phần: Compensation Handler và Compensation Trigger

Compensation trong BPMN được xây dựng từ hai phần phối hợp với nhau:

1. Compensation Handler (bộ xử lý bù trừ) — Đây là logic undo gắn liền với một activity cụ thể. Về mặt ký hiệu, nó là một Boundary Event (sự kiện biên) loại Compensation, đính vào cạnh của activity gốc, và từ đó nối tới một task xử lý compensation (thường vẽ với ký hiệu hai mũi tên tua ngược ◄◄). Ví dụ: activity "Đặt phòng khách sạn" sẽ có một Compensation Handler là task "Hủy đặt phòng khách sạn". Handler này không chạy ngay khi activity hoàn thành — nó chỉ "nằm chờ" cho đến khi có ai đó kích hoạt nó. Điểm quan trọng: handler có thể nhận lại dữ liệu của activity gốc (như mã booking) để biết chính xác phải hủy cái gì.

2. Compensation Trigger (cò kích hoạt bù trừ) — Đây là tín hiệu phát ra lệnh "hãy compensate". Nó có thể là một Throw Compensation Event (sự kiện ném compensation, vẽ với ký hiệu hai tam giác tua ngược màu đen) đặt ở giữa quy trình, hoặc là một End Event loại Compensation. Khi token chạy đến trigger này, BPMN engine sẽ tìm tất cả các activity đã hoàn thành trong phạm vi (scope) và lần lượt gọi Compensation Handler tương ứng của chúng — theo thứ tự ngược lại với thứ tự chúng đã thực thi (Last-In-First-Out). Đây là điểm rất tinh tế: undo phải đi lùi đúng thứ tự, vì bước sau có thể phụ thuộc vào bước trước.

Quy tắc thứ tự LIFO và phạm vi (scope)

Khi một Throw Compensation được kích hoạt cho cả một Sub-process, engine sẽ compensate các activity con theo thứ tự ngược lại với thứ tự thực thi. Nếu quy trình chạy A → B → C, thì compensation sẽ chạy undo-C → undo-B → undo-A. Logic này giống hệt cách bạn tháo dỡ một công trình: lắp móng trước rồi mới lắp tường, nhưng khi đập thì phải đập tường trước rồi mới phá móng.

Compensation cũng bị giới hạn bởi scope: một Throw Compensation chỉ kích hoạt được các handler trong cùng phạm vi của nó. Nếu bạn ném compensation bên trong một Sub-process, nó chỉ ảnh hưởng tới các activity trong Sub-process đó, không "leo" ra ngoài quy trình cha. Hiểu rõ scope giúp bạn kiểm soát chính xác phần nào của quy trình sẽ bị hoàn tác.

Tình huống thực tế

Tình huống 1 — Đặt tour trọn gói tại một công ty du lịch (bối cảnh Việt Nam)

Một công ty lữ hành ở TP.HCM, giả định tên Saigon Voyage, xây dựng quy trình tự động "Đặt combo du lịch" gồm ba activity tuần tự: (1) Giữ vé máy bay qua API Vietnam Airlines, (2) Giữ phòng khách sạn qua hệ thống đối tác, (3) Thuê xe đưa đón. Mỗi bước đều có Compensation Handler riêng đính ở biên: "Hủy giữ vé", "Hủy đặt phòng", "Hủy thuê xe".

Một ngày cao điểm Tết, hệ thống xử lý đơn của khách hàng tên Minh: giữ vé thành công (đã trừ 4.200.000đ tiền giữ chỗ), giữ phòng thành công (giữ 2 đêm tại Đà Lạt), nhưng đến bước thuê xe thì đối tác báo "hết xe khả dụng". Bước 3 ném ra lỗi. Quy trình bắt lỗi bằng một Error Boundary Event, và từ đó nối tới một Throw Compensation Event ở scope của Sub-process "Đặt combo".

Khi compensation được kích hoạt, engine chạy theo thứ tự LIFO: bước 3 chưa hoàn thành nên không có gì để undo; nó gọi handler của bước 2 ("Hủy đặt phòng" — giải phóng 2 đêm phòng Đà Lạt), rồi gọi handler của bước 1 ("Hủy giữ vé" — hoàn lại 4.200.000đ). Kết quả: khách hàng Minh không bị mắc kẹt với một nửa combo, không bị "treo" tiền giữ vé, và hệ thống tự gửi email xin lỗi kèm đề xuất ngày khác.

Bài học: Không có Compensation, đội vận hành Saigon Voyage sẽ phải xử lý tay hàng trăm đơn "dở dang" mỗi mùa cao điểm, vừa tốn nhân sự vừa dễ sai sót gây khiếu nại. Compensation biến việc undo phức tạp thành một phần được thiết kế sẵn, tự động và đáng tin cậy.

Tình huống 2 — Quy trình mở tài khoản và phát hành thẻ tại ngân hàng

Một ngân hàng số tại Việt Nam triển khai quy trình onboarding khách hàng mới gồm: (1) Tạo hồ sơ KYC, (2) Mở tài khoản thanh toán trên core banking, (3) Phát hành thẻ ghi nợ ảo, (4) Gửi thông báo kích hoạt qua SMS. Bước 4 phụ thuộc vào một nhà cung cấp SMS bên ngoài. Trong một sự cố hiếm gặp, sau khi đã mở tài khoản và phát hành thẻ thành công, hệ thống phát hiện hồ sơ KYC của khách hàng bị trùng CCCD với một tài khoản gian lận đã có trong blacklist — đây là một kiểm tra hậu kỳ chạy ở bước 4.

Vì lý do tuân thủ (compliance), quy trình không được phép giữ lại tài khoản và thẻ đã tạo. Một Throw Compensation được kích hoạt. Engine chạy LIFO: handler bước 3 "Hủy/khóa thẻ vừa phát hành", handler bước 2 "Đóng tài khoản thanh toán", handler bước 1 "Đánh dấu hồ sơ KYC là rejected và ghi log audit". Lưu ý điểm tinh tế ở đây: handler bước 1 không xóa hồ sơ mà chỉ chuyển trạng thái — vì lý do audit, ngân hàng phải giữ lại bằng chứng đã từng có nỗ lực mở tài khoản này.

Bài học: Compensation Handler không nhất thiết phải "xóa sạch dấu vết". Trong môi trường có yêu cầu tuân thủ, undo nghiệp vụ thường là chuyển trạng thái + ghi log thay vì xóa thật. Bạn thiết kế handler dựa trên ý nghĩa nghiệp vụ, không phải dựa trên việc "đảo ngược kỹ thuật" một cách máy móc.

Tình huống 3 — Đặt hàng thương mại điện tử với Saga pattern (bối cảnh Đông Nam Á)

Một sàn TMĐT khu vực Đông Nam Á chạy quy trình checkout phân tán qua nhiều microservice: Service Kho, Service Thanh toán, Service Vận chuyển. Mỗi service là một hệ thống độc lập, không chia sẻ database, nên không thể dùng một transaction chung. Họ dùng BPMN với Compensation để hiện thực hóa Saga.

Quy trình: (1) Trừ tồn kho 1 chiếc điện thoại, (2) Trừ tiền 12.990.000đ qua ví điện tử, (3) Tạo đơn vận chuyển. Ở bước 3, đối tác vận chuyển từ chối vì địa chỉ giao nằm ngoài vùng phủ. Throw Compensation kích hoạt: handler bước 2 tạo giao dịch hoàn tiền 12.990.000đ vào ví (không phải "rollback" — vì giao dịch gốc đã commit ở phía ví, nên buộc phải tạo giao dịch ngược), handler bước 1 cộng trả 1 chiếc vào tồn kho.

Có một chi tiết quan trọng: handler hoàn tiền phải idempotent (chạy nhiều lần vẫn cho cùng kết quả). Vì trong hệ phân tán, tín hiệu compensation có thể bị gửi lại do lỗi mạng. Nếu handler không idempotent, khách hàng có thể được hoàn tiền hai lần — gây thất thoát thật.

Bài học: Compensation là cách BPMN diễn đạt Saga pattern một cách trực quan thay vì giấu trong code. Nhưng người thiết kế phải tự đảm bảo các handler có tính idempotent và xử lý được trường hợp "compensation của compensation thất bại" — vì bản thân việc hoàn tiền cũng có thể lỗi.

Hướng dẫn từng bước

Để đưa Compensation vào quy trình của bạn, hãy làm theo các bước sau:

  • Xác định activity nào cần undo. Rà soát quy trình, đánh dấu mọi activity tạo ra "thay đổi có thật" ở hệ thống bên ngoài: trừ tiền, giữ kho, đặt chỗ, tạo hồ sơ. Những activity chỉ đọc dữ liệu (như "kiểm tra tồn kho") thì không cần compensation.
  • Viết logic undo nghiệp vụ cho từng activity. Với mỗi activity đã đánh dấu, trả lời câu hỏi: "Nếu việc này đã xong nhưng cần đảo ngược, tôi phải làm gì?". Nhớ rằng đó là hành động nghiệp vụ ngược lại (hoàn tiền, giải phóng phòng), không phải ROLLBACK kỹ thuật.
  • Đính Compensation Boundary Event vào activity gốc. Trên sơ đồ, gắn một Boundary Event loại Compensation (vòng tròn kép với ký hiệu tua ngược) vào cạnh activity, rồi nối tới task compensation tương ứng.
  • Đặt Compensation Trigger đúng chỗ. Quyết định nơi quy trình cần "đi lùi". Thường đặt một Throw Compensation Event sau nhánh xử lý lỗi, hoặc một End Compensation Event để hủy toàn bộ scope.
  • Xác định scope rõ ràng. Đảm bảo trigger nằm trong cùng phạm vi với các handler bạn muốn kích hoạt. Nếu cần undo cả một nhóm bước, gom chúng vào một Sub-process và compensate ở cấp Sub-process.
  • Đảm bảo handler idempotent và truyền đúng dữ liệu. Mỗi handler cần nhận được dữ liệu định danh từ activity gốc (mã booking, mã giao dịch) và phải chịu được việc bị gọi lặp lại mà không gây tác dụng phụ.
  • Mô phỏng và kiểm thử các kịch bản lỗi. Chạy thử trên công cụ (Camunda, Bizagi) với tình huống bước cuối thất bại, kiểm tra xem thứ tự undo có đúng LIFO không và trạng thái cuối có sạch không.

Lỗi thường gặp & mẹo

Lỗi 1 — Nhầm Compensation với Error Handling. Nhiều người vẽ một Error Boundary Event rồi nghĩ là đã "hoàn tác". Không phải. Error Handling chỉ rẽ nhánh khi lỗi xảy ra; nó không tự động đảo ngược các bước đã hoàn thành. Bạn cần kết hợp: bắt lỗi bằng Error Event, rồi từ nhánh lỗi ném Compensation để thực sự undo.

Lỗi 2 — Gắn Compensation cho activity chưa hoàn thành. Compensation chỉ áp dụng cho activity đã hoàn thành thành công. Đính handler vào một activity mà bạn kỳ vọng nó undo "khi đang chạy" là sai mô hình — trường hợp đó dùng Error hoặc Cancel.

Lỗi 3 — Quên tính idempotent. Trong hệ phân tán, tín hiệu compensation có thể đến hai lần. Handler hoàn tiền không idempotent sẽ hoàn tiền hai lần. Mẹo: luôn dùng một khóa định danh (idempotency key) cho mỗi lần compensate.

Lỗi 4 — Bỏ qua việc compensation cũng có thể thất bại. Việc hoàn tiền cũng cần gọi API và cũng có thể lỗi. Hãy thiết kế cơ chế retry hoặc đẩy vào hàng đợi "cần xử lý tay" cho những compensation thất bại, thay vì giả định nó luôn thành công.

Mẹo vàng: Hãy đặt tên task compensation theo đúng hành động nghiệp vụ ngược: nếu task gốc là "Giữ phòng" thì handler là "Hủy giữ phòng" — không phải "Compensation 1". Tên rõ ràng giúp cả đội đọc sơ đồ hiểu ngay logic undo mà không cần giải thích.

Bài tập thực hành

Bài tập 1 — Vẽ sơ đồ. Cho quy trình đăng ký gói cước viễn thông gồm 3 bước tuần tự: (1) Đăng ký SIM, (2) Trừ tiền cọc 200.000đ, (3) Kích hoạt gói data. Giả sử bước 3 thất bại vì hệ thống nhà mạng quá tải. Hãy vẽ sơ đồ BPMN có đầy đủ: Compensation Boundary Event cho bước 1 và bước 2, các task undo tương ứng, và một Throw Compensation được kích hoạt sau nhánh lỗi của bước 3. Ghi rõ thứ tự các handler sẽ chạy.

Bài tập 2 — Phân tích thứ tự. Một Sub-process chạy bốn activity A → B → C → D đều thành công, nhưng sau đó một quy tắc nghiệp vụ buộc phải hủy toàn bộ. Hãy liệt kê thứ tự các Compensation Handler được engine gọi, và giải thích tại sao thứ tự đó (LIFO) lại quan trọng — cho một ví dụ về điều gì sẽ hỏng nếu undo sai thứ tự.

Bài tập 3 — Thiết kế handler an toàn. Cho activity "Trừ tiền 5.000.000đ qua cổng thanh toán". Hãy viết mô tả ngắn (3–5 câu) cho Compensation Handler của nó, đảm bảo: (a) tạo giao dịch hoàn tiền chứ không "rollback", (b) idempotent, (c) ghi log để audit. Sau đó nêu một tình huống mà chính handler hoàn tiền này cũng thất bại và cách bạn xử lý.

Tóm tắt

Compensation là cơ chế của BPMN dùng để hoàn tác (undo) một activity đã hoàn thành thành công — khác hẳn với Error Handling vốn chỉ rẽ nhánh khi lỗi phát sinh. Nó là cách biểu diễn trực quan của Saga pattern trong các quy trình phân tán, nơi không thể dùng một database transaction chung để rollback.

Hai thành phần cốt lõi là Compensation Handler (logic undo gắn vào biên activity qua Boundary Event) và Compensation Trigger (Throw/End Compensation Event phát lệnh đi lùi). Khi kích hoạt, engine gọi các handler theo thứ tự ngược lại (LIFO) và trong đúng scope đã định.

Ghi nhớ ba nguyên tắc khi áp dụng: chỉ compensate việc đã hoàn thành, undo là hành động nghiệp vụ ngược chứ không phải rollback kỹ thuật, và handler phải idempotent vì có thể bị gọi lại. Qua các tình huống đặt tour, mở tài khoản ngân hàng và checkout TMĐT, bạn đã thấy Compensation giúp quy trình không bao giờ để lại trạng thái dở dang — điều quyết định sự khác biệt giữa một hệ thống "chạy được trong demo" và một hệ thống đáng tin cậy trong vận hành thật.