Chào bạn,
Trong bài học trước, chúng ta đã học cách sử dụng Signal Events để thực hiện giao tiếp một-nhiều (broadcast), một cơ chế rất mạnh mẽ để đồng bộ hóa nhiều quy trình. Bạn đã thấy cách một tín hiệu duy nhất có thể kích hoạt hoặc thay đổi luồng của hàng loạt process instance đang chạy.
Tuy nhiên, trong quá trình thực thi một Service Task, đặc biệt là khi nó giao tiếp với các hệ thống bên ngoài như REST API hoặc cơ sở dữ liệu, mọi thứ không phải lúc nào cũng diễn ra suôn sẻ. Các lỗi có thể xảy ra: API bị sập, dữ liệu đầu vào không hợp lệ, tài khoản không đủ tiền, v.v. Việc để những lỗi này làm sập toàn bộ quy trình là điều không thể chấp nhận được trong một hệ thống sản xuất.
Vì vậy, mục tiêu của bài học hôm nay là: Sử dụng Error Boundary Event để bắt và xử lý các exception từ một Service Task. Bạn sẽ học cách xây dựng các quy trình có khả năng phục hồi, có thể dự đoán và xử lý các "lỗi nghiệp vụ" (business errors) một cách có kiểm soát, thay vì để chúng trở thành các sự cố kỹ thuật (technical incidents).
1. Phân biệt Lỗi nghiệp vụ và Lỗi kỹ thuật
Trước khi đi vào thực hành, điều quan trọng nhất là phải phân biệt được hai loại lỗi chính mà bạn sẽ gặp phải. Là một developer, việc hiểu rõ sự khác biệt này sẽ giúp bạn quyết định cách xử lý phù hợp.
-
Lỗi kỹ thuật (Technical Errors): Đây là những lỗi không lường trước được, thường liên quan đến cơ sở hạ tầng. Ví dụ: mất kết nối mạng, API của bên thứ ba tạm thời không khả dụng, lỗi hết bộ nhớ.
- Cách xử lý điển hình: Tự động thử lại (retry) một vài lần. Nếu sau nhiều lần thử lại vẫn thất bại, hệ thống sẽ tạo ra một "incident" trong Camunda Cockpit để người vận hành can thiệp thủ công. Những logic này thường không được vẽ trực tiếp trên sơ đồ BPMN để tránh làm phức tạp hóa mô hình.
-
Lỗi nghiệp vụ (Business Errors): Đây là những lỗi có thể dự đoán được, là một phần của luồng logic nghiệp vụ. Ví dụ: "khách hàng không đủ tín dụng", "sản phẩm đã hết hàng", "mã giảm giá không hợp lệ".
- Cách xử lý điển hình: Quy trình cần có một luồng xử lý thay thế rõ ràng. Ví dụ, nếu khách hàng không đủ tín dụng, quy trình sẽ chuyển sang bước "Thông báo từ chối" thay vì dừng lại. Những luồng thay thế này phải được mô hình hóa rõ ràng trên sơ đồ BPMN bằng cách sử dụng các công cụ như
Error Boundary Event.
- Cách xử lý điển hình: Quy trình cần có một luồng xử lý thay thế rõ ràng. Ví dụ, nếu khách hàng không đủ tín dụng, quy trình sẽ chuyển sang bước "Thông báo từ chối" thay vì dừng lại. Những luồng thay thế này phải được mô hình hóa rõ ràng trên sơ đồ BPMN bằng cách sử dụng các công cụ như
Video sau đây sẽ giúp bạn hình dung rõ hơn về hai loại lỗi này.
Video này của Camunda giải thích sự khác biệt cơ bản giữa lỗi nghiệp vụ (business errors) được xử lý bằng các ký hiệu BPMN và lỗi kỹ thuật (technical errors) dẫn đến các incident.
Hãy xem từ 01:28 đến 02:11. Chú ý cách video minh họa Error Boundary Event như một công cụ để xử lý business error và cách các lỗi kỹ thuật được xử lý ngầm bằng cơ chế retry.
Bài học này sẽ tập trung hoàn toàn vào việc xử lý lỗi nghiệp vụ bằng Error Boundary Event.
2. Triển khai Error Boundary Event: Ném và Bắt
Việc xử lý lỗi bằng Error Boundary Event bao gồm hai bước chính, giống như cặp try-catch trong lập trình:
- Ném (Throwing): Mã Java bên trong
Service Taskcủa bạn phải chủ động "ném" ra một lỗi nghiệp vụ khi phát hiện một điều kiện cụ thể. - Bắt (Catching): Một
Error Boundary Eventđược gắn vàoService Taskđó trong mô hình BPMN sẽ "bắt" lỗi này và chuyển hướng quy trình sang một luồng xử lý khác.
Bước 1: Ném lỗi từ Java Delegate
Trong Camunda, bạn không ném một Exception thông thường. Thay vào đó, bạn ném một đối tượng đặc biệt là org.camunda.bpm.engine.delegate.BpmnError. Đối tượng này mang một mã lỗi (error code), đóng vai trò như một chiếc chìa khóa để liên kết với Error Boundary Event trong mô hình.
// Giả sử trong một Service Task xử lý thanh toán
if (customerBalance < orderAmount) {
// Ném một lỗi nghiệp vụ với một mã lỗi cụ thể
throw new BpmnError("INSUFFICIENT_FUNDS", "Khách hàng không đủ tiền để thực hiện giao dịch.");
}
"INSUFFICIENT_FUNDS"là mã lỗi. Đây là thông tin quan trọng nhất."Khách hàng..."là thông điệp lỗi, có thể được lưu lại để phân tích sau.
Bước 2: Bắt lỗi trong Camunda Modeler
Trong mô hình BPMN, bạn sẽ gắn một Error Boundary Event vào Service Task có khả năng ném lỗi.

Cấu hình chi tiết như sau:
- Kéo một
Boundary Eventvà thả nó lênService Task. - Click chuột phải vào nó, chọn
Change Type->Error Boundary Event. Biểu tượng sẽ chuyển thành hình tia sét. - Chọn
Error Boundary Eventvà vào Properties Panel, tabDetails:- Error Code Variable: Nhập vào một chuỗi ký tự chính xác khớp với mã lỗi bạn đã định nghĩa trong mã Java (ví dụ:
INSUFFICIENT_FUNDS). - Error Message Variable (Tùy chọn): Đặt tên cho một biến quy trình (ví dụ:
paymentErrorMessage) để lưu lại thông điệp lỗi.
- Error Code Variable: Nhập vào một chuỗi ký tự chính xác khớp với mã lỗi bạn đã định nghĩa trong mã Java (ví dụ:
- Vẽ một luồng đi từ
Error Boundary Eventđến các bước xử lý lỗi (ví dụ: mộtUser Task"Thông báo cho khách hàng").
Để xem một ví dụ hoàn chỉnh về cách ném và bắt lỗi, hãy xem video hướng dẫn sau đây.
Camunda Platform 7 Tutorial: Running Timer Events, Catching BPMN Error Events in Spring Boot Project
Video này của Camunda sẽ hướng dẫn bạn từng bước cách implement cơ chế xử lý lỗi: từ việc viết code Java để ném BpmnError, đến việc mô hình hóa Error Boundary Event để bắt nó.
Hãy xem từ 06:03 đến 08:56. Chú ý các bước sau: (06:03 - 07:21) Cách thêm logic vào Java Delegate để kiểm tra một điều kiện và throw new BpmnError("fall_overboard"). (07:21 - 07:55) Cách thêm một Error Boundary Event vào Service Task trong Modeler. (07:55 - 08:56) Cách cấu hình Error Boundary Event: đặt tên, và quan trọng nhất là thiết lập 'Error Code' để khớp với mã lỗi được ném từ code. Đồng thời, cấu hình một biến để lưu thông điệp lỗi.
3. Thực hành: Xử lý lỗi thanh toán không thành công
Bây giờ, hãy áp dụng kiến thức vào một kịch bản thực tế. Chúng ta sẽ xây dựng một quy trình xử lý đơn hàng, trong đó bước thanh toán có thể thất bại do không đủ tiền.
Kịch bản:
- Người dùng đặt hàng và nhập thông tin thanh toán.
- Hệ thống gọi đến
Service Task"Xử lý thanh toán". Service Tasknày sẽ kiểm tra một biếncustomerBalance.- Nếu
customerBalanceđủ, quy trình tiếp tục đến bước "Giao hàng". - Nếu
customerBalancekhông đủ, nó sẽ ném lỗiINSUFFICIENT_FUNDS.
- Nếu
Error Boundary Eventsẽ bắt lỗi này và chuyển quy trình sang bước "Thông báo thanh toán thất bại" cho người dùng.
Bước 1: Mô hình hóa quy trình
-
Trong Camunda Modeler, tạo một quy trình như sau:
- Start Event
- User Task:
Xác nhận đơn hàng - Service Task:
Xử lý thanh toán - End Event:
Hoàn tất - User Task:
Thông báo thanh toán thất bại - End Event:
Đóng đơn hàng
-
Gắn một Error Boundary Event vào
Service Task"Xử lý thanh toán". -
Nối
Error Boundary EventđếnUser Task"Thông báo thanh toán thất bại". Nối task này đếnEnd Event"Đóng đơn hàng". -
Nối luồng thành công từ "Xử lý thanh toán" đến
End Event"Hoàn tất".
Quy trình của bạn sẽ trông giống như thế này:

Bước 2: Cấu hình các phần tử
- Chọn Service Task
Xử lý thanh toán:- Implementation:
Java Class - Java Class:
com.example.camunda.delegates.PaymentDelegate
- Implementation:
- Chọn Error Boundary Event:
- Trong Properties Panel, tab
Details: - Name:
Lỗi không đủ tiền - Error Code:
INSUFFICIENT_FUNDS - Error Message Variable:
paymentError
- Trong Properties Panel, tab
Bước 3: Viết mã Java Delegate
Tạo lớp PaymentDelegate.java trong dự án Spring Boot của bạn.
package com.example.camunda.delegates;
import org.camunda.bpm.engine.delegate.BpmnError;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component("PaymentDelegate")
public class PaymentDelegate implements JavaDelegate {
private final Logger LOGGER = LoggerFactory.getLogger(PaymentDelegate.class);
@Override
public void execute(DelegateExecution execution) throws Exception {
// Lấy các biến từ quy trình
Double orderTotal = (Double) execution.getVariable("orderTotal");
Double customerBalance = (Double) execution.getVariable("customerBalance");
LOGGER.info(">>> Processing payment for order total: {}. Customer balance: {}", orderTotal, customerBalance);
if (customerBalance < orderTotal) {
LOGGER.error("!!! Insufficient funds. Throwing BPMN Error.");
// Ném lỗi nghiệp vụ với mã lỗi và thông điệp
throw new BpmnError("INSUFFICIENT_FUNDS", "Số dư tài khoản không đủ để thanh toán đơn hàng.");
}
// Nếu không có lỗi, thực hiện logic thanh toán thành công
LOGGER.info(">>> Payment successful!");
double newBalance = customerBalance - orderTotal;
execution.setVariable("customerBalance", newBalance);
}
}
Bước 4: Deploy và kiểm thử
- Deploy quy trình và khởi chạy ứng dụng.
- Kiểm thử luồng thành công:
- Vào Tasklist, khởi tạo một process instance mới.
- Trong form khởi tạo, thêm 2 biến:
orderTotal(Type:Double, Value:100) vàcustomerBalance(Type:Double, Value:500). - Start process. Hoàn thành task
Xác nhận đơn hàng. - Vào Cockpit, bạn sẽ thấy quy trình đã đi qua
Xử lý thanh toánvà kết thúc tạiHoàn tất. BiếncustomerBalanceđã được cập nhật thành400.
- Kiểm thử luồng lỗi:
- Khởi tạo một process instance khác.
- Lần này, đặt biến:
orderTotal(Type:Double, Value:100) vàcustomerBalance(Type:Double, Value:50). - Start process. Hoàn thành task
Xác nhận đơn hàng. - Lần này, vào Cockpit, bạn sẽ thấy token đã đi theo con đường của
Error Boundary Eventvà đang dừng lại ở taskThông báo thanh toán thất bại. - Kiểm tra tab
Variablescủa instance này, bạn sẽ thấy biếnpaymentErrorđã được tạo với giá trị là "Số dư tài khoản không đủ để thanh toán đơn hàng.".
4. Khi nào dùng Error Event, khi nào dùng Gateway?
Một câu hỏi quan trọng mà các developer thường đặt ra: "Tại sao không dùng một Exclusive Gateway sau Service Task để kiểm tra kết quả và rẽ nhánh?".
Đây là một câu hỏi rất hay, và câu trả lời nằm ở ngữ nghĩa của nghiệp vụ.
Dealing with problems and exceptions - Camunda 8 Docs
Tài liệu chính thức của Camunda cung cấp một hướng dẫn rất rõ ràng về việc khi nào nên dùng Gateway và khi nào nên dùng Error Event. Đây là kiến thức quan trọng để thiết kế các quy trình rõ ràng và đúng đắn.
Hãy đọc kỹ phần "Distinguishing between exceptions and results". Chú ý quy tắc cốt lõi: Sử dụng Gateway khi bạn xử lý một kết quả mong đợi của một hoạt động. Ví dụ: task "Kiểm tra tín dụng khách hàng" có kết quả là 'đủ' hoặc 'không đủ'. Cả hai đều là kết quả hợp lệ. Sử dụng Error Event khi một lỗi ngăn cản hoạt động đạt được kết quả mong đợi. Ví dụ: task "Kiểm tra tín dụng" không thể hoàn thành vì ID khách hàng không tồn tại. Hoạt động đã thất bại.
Trong ví dụ thanh toán của chúng ta, việc "không đủ tiền" có thể được xem như một kết quả (dùng Gateway) hoặc một lỗi (dùng Error Event). Việc sử dụng Error Boundary Event nhấn mạnh rằng đây là một tình huống ngoại lệ, một sự thất bại trong việc hoàn thành hành động "Xử lý thanh toán". Cách tiếp cận này thường làm cho sơ đồ BPMN rõ ràng hơn về "happy path".
Tổng kết
Qua bài học này, bạn đã trang bị thêm một công cụ cực kỳ quan trọng để xây dựng các quy trình nghiệp vụ mạnh mẽ và linh hoạt.
Những điểm chính cần ghi nhớ:
- Phân biệt rõ ràng giữa lỗi kỹ thuật (xử lý bằng retry, incident) và lỗi nghiệp vụ (xử lý bằng luồng thay thế trong BPMN).
Error Boundary Eventlà công cụ chính để mô hình hóa việc xử lý lỗi nghiệp vụ.- Quy trình xử lý lỗi gồm 2 bước: Ném một
BpmnErrorvới mộterrorCodetừ trongJavaDelegate, và Bắt nó bằng mộtError Boundary Eventđược cấu hình với cùngerrorCodeđó. - Việc sử dụng Error Event hay Gateway phụ thuộc vào việc bạn xem một tình huống là "kết quả mong đợi" hay là một "lỗi ngoại lệ".
Xem trước bài học tiếp theo
Trong các Service Task và Gateway của chúng ta, các quy tắc nghiệp vụ (business rules) như "khách hàng nào được coi là VIP?", "mức giảm giá cho đơn hàng trên 1000$ là bao nhiêu?" thường được hard-code trong Java hoặc viết dưới dạng biểu thức (Expression Language). Khi các quy tắc này trở nên phức tạp hoặc thay đổi thường xuyên, việc duy trì chúng trong code trở nên khó khăn.
Trong bài học tiếp theo, chúng ta sẽ tìm hiểu về DMN (Decision Model and Notation), một tiêu chuẩn cho phép bạn tách biệt các quy tắc nghiệp vụ ra khỏi quy trình, quản lý chúng dưới dạng các bảng quyết định trực quan và dễ thay đổi mà không cần deploy lại code.