Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

BPMN 2.0

Tung Bui Thanh (TungDaDev)32 mins read

Bpmn.webp

# giới thiệu

BPMN 2.0 (Business Process Model and Notation) là chuẩn quốc tế do OMG (Object Management Group) ban hành. Nó không chỉ là công cụ vẽ flowchart: đây là bộ ký hiệu đồ họa đóng vai trò "Ubiquitous Language" (ngôn ngữ chung) cho cả Business Analyst (BA) lẫn Developer. BPMN là cầu nối giữa "nghiệp vụ muốn gì" (Business Intent) và "hệ thống chạy thế nào" (System Execution).

Ở các hệ thống Enterprise lớn như Ngân hàng, Bảo hiểm hay Logistics, BPMN là công nghệ nền (core technology). Nó cho phép thiết kế và điều phối (orchestrate) các workflow phức tạp qua những Workflow Engine như Camunda, Flowable hay Activiti.

Quy trình nào cũng đi qua một lifecycle: Design → Deploy → Execute → Monitor. Mấu chốt là hình vẽ trên UI được "backing" bởi định dạng BPMN 2.0 XML chuẩn, nên engine có thể parse và chạy trực tiếp.

Tại sao một Senior/Principal Backend Engineer nên nắm chắc BPMN?

Làm việc với hệ thống enterprise, Developer không chỉ viết CRUD. Hiểu BPMN giúp bạn:

  • Audit & alignment: đọc hiểu và review chéo (cross-review) các process definitions do BA thiết kế, xem có khả thi về mặt kỹ thuật không.

  • Core implementation: tự implement các system tasks, event listeners và Java delegates để gắn code vào các node của quy trình.

  • Troubleshooting: debug các luồng process execution khi state bị tắc (state machine issues).

  • Performance tuning: tối ưu các quy trình lớn, xử lý concurrency và database locking của engine.

  • System integration: thiết kế tích hợp giữa BPMN Engine và hệ Microservices, nhất là dùng BPMN để giải bài toán Distributed Transactions (VD: Orchestration Saga Pattern).

# kiến trúc bpmn 2.0

Thử hình dung bạn thiết kế luồng xử lý cho hệ thống Loan Origination (khởi tạo khoản vay). Bạn cần xác định: điểm kích hoạt (khách hàng submit hồ sơ), các node xử lý nghiệp vụ (thẩm định rủi ro, phê duyệt tín dụng), các Gateway rẽ nhánh (chấp thuận hay từ chối?) và điểm kết thúc (giải ngân hoặc gửi thông báo). BPMN 2.0 chuẩn hóa tất cả các khái niệm này thành một bộ ký hiệu duy nhất, tức là vendor-agnostic: dùng tool nào để vẽ thì developer và BA nhìn vào vẫn hiểu giống nhau.

Điểm mà nhiều developer hay đánh giá thấp: BPMN 2.0 không phải một bức tranh để ngắm.

Nhìn từ góc độ hệ thống, nó là một Executable Specification (đặc tả chạy được). File BPMN XML là Single Source of Truth (nguồn sự thật duy nhất): vừa render ra hình trên màn hình, vừa là tập lệnh để các engine như Camunda, Activiti hay Flowable parse và quản lý trạng thái (state) trên server. Diagram trên UI và logic chạy dưới backend thực chất là CÙNG MỘT FILE.

Để có khả năng thực thi đó, BPMN 2.0 chia thành 5 nhóm elements chính.

bpmn-elements

Bạn không cần thuộc hết: 80% quy trình, kể cả phức tạp, chỉ xoay quanh khoảng 10-15 elements dùng đi dùng lại. Phần tiếp theo đi vào từng nhóm kèm ví dụ.

# events - "điều gì xảy ra"

Events là những sự kiện xảy ra trong vòng đời process. Chúng được vẽ bằng hình tròn, và viền của hình tròn cho biết loại event:

  • Viền mảnh (thin) = Start Event
  • Viền đôi (double) = Intermediate Event
  • Viền dày (bold) = End Event

Bên trong hình tròn, icon cho biết trigger type: đồng hồ (timer), phong bì (message), tam giác (signal), tia sét gấp khúc (error)...

Một điểm nhiều người bỏ qua: events chia làm hai cơ chế ngược nhau, Catching (chờ sự kiện đến) và Throwing (phát ra sự kiện).

  • Catching (lắng nghe/chờ): process ở trạng thái "blocking" hoặc "waiting" để đợi tín hiệu từ bên ngoài (giống Consumer lắng nghe một topic).
  • Throwing (phát ra): process chủ động "bắn" một tín hiệu để báo cho các hệ thống xung quanh (giống Producer).

Phân biệt rõ hai cơ chế này rất quan trọng khi thiết kế luồng giao tiếp giữa các hệ thống (Inter-process Communication).

Trong thực tế hay gặp nhất:

  • Timer events: xử lý SLA và Timeout. Ví dụ: task duyệt hồ sơ treo quá 48h thì tự trigger luồng escalation báo lên cấp quản lý.
  • Message events: chủ yếu cho tích hợp. Dùng để hứng Webhook từ hệ thống external hoặc nhận tín hiệu giữa các service (inter-service triggers) qua message broker như Kafka hay RabbitMQ.
  • Error events: xử lý lỗi. Ví dụ: call API thanh toán thất bại thì trigger Error Event để chạy luồng bù trừ (Compensating Transaction trong Saga), rollback về trạng thái trước hoặc rẽ sang luồng xử lý thủ công.

# start events - khởi đầu process

EventKý hiệuMô tảUse case
None Start(empty circle)Manually triggeredUser click "Start Process"
Timer Startvới clockTriggered theo scheduleDaily report generation
Message Startvới envelopeTriggered bởi messageOrder received from API
Signal Startvới triangleTriggered bởi broadcast signalSystem-wide event
Conditional Startvới paperTriggered khi condition = trueKPI threshold exceeded
<!-- None Start Event -->
<startEvent id="start" name="Start Process"/>
 
<!-- Timer Start Event: chạy mỗi ngày lúc 8:00 AM -->
<startEvent id="timerStart" name="Daily Trigger">
   <timerEventDefinition>
       <timeCycle>0 0 8 * * ?</timeCycle>
   </timerEventDefinition>
</startEvent>
 
<!-- Message Start Event -->
<startEvent id="msgStart" name="Order Received">
   <messageEventDefinition messageRef="orderMessage"/>
</startEvent>

# intermediate events - giữa process

Intermediate events xảy ra giữa start và end - chờ event, ném signal, delay process.

<!-- Timer Intermediate (wait/delay) -->
<intermediateCatchEvent id="wait3days" name="Wait 3 Days">
   <timerEventDefinition>
       <timeDuration>P3D</timeDuration>  <!-- ISO 8601: 3 days -->
   </timerEventDefinition>
</intermediateCatchEvent>
 
<!-- Message Intermediate Catch (wait for message) -->
<intermediateCatchEvent id="waitApproval" name="Wait for Approval">
   <messageEventDefinition messageRef="approvalMessage"/>
</intermediateCatchEvent>
 
<!-- Signal Throw (broadcast to all processes listening) -->
<intermediateThrowEvent id="notifyAll" name="Notify All">
   <signalEventDefinition signalRef="processCompleteSignal"/>
</intermediateThrowEvent>
 
<!-- Boundary Timer (attached to task — deadline/timeout) -->
<boundaryEvent id="deadline" attachedToRef="reviewTask" cancelActivity="true">
   <timerEventDefinition>
       <timeDuration>PT48H</timeDuration>  <!-- 48 hours timeout -->
   </timerEventDefinition>
</boundaryEvent>
<!-- Nếu reviewTask không xong trong 48h → cancel task, đi đường khác -->

# end events - kết thúc process

EventMô tảEffect
None EndProcess instance kết thúc bình thườngTerminate path
Terminate EndKết thúc TẤT CẢ paths (parallel) ngay lập tứcKill toàn bộ instance
Error EndThrow error (catch bởi boundary error event)Error propagation
Message EndGửi message khi kết thúcNotify external
Signal EndBroadcast signal khi kết thúcNotify all listeners
<!-- Normal End -->
<endEvent id="end" name="Process Complete"/>
 
<!-- Terminate End: kill tất cả parallel paths -->
<endEvent id="terminateEnd" name="Abort All">
   <terminateEventDefinition/>
</endEvent>
 
<!-- Error End: throw error lên parent process -->
<endEvent id="errorEnd" name="Payment Failed">
   <errorEventDefinition errorRef="paymentError"/>
</endEvent>

# activities - "công việc được thực hiện"

Nếu Events cho biết "điều gì vừa kích hoạt" và Gateways quyết định "rẽ nhánh nào", thì Activities là nơi công việc thực sự được làm. Với backend engineer, đây là phần mình dành nhiều thời gian nhất, vì mỗi Activity trên bản vẽ sẽ map trực tiếp xuống một khối code xử lý logic.

Activities luôn là hình chữ nhật bo góc. Icon nhỏ ở góc trên bên trái cho biết loại tác vụ: hình người (User Task), bánh răng (Service Task), hoặc cuộn giấy (Script Task).

Điểm quan trọng nằm ở cách Engine xử lý trạng thái:

  • User Task (wait state): khi chạm node này, BPMN Engine tạo một trạng thái chờ ("wait state"). Trạng thái process được persist xuống database và luồng thực thi tạm ngưng, chờ con người tương tác (điền form, bấm approve trên UI).
  • Service Task (integration point): điểm giao giữa quy trình và hệ thống backend. Service Task là nơi bạn "plug" code Java vào. Nó có thể gọi external REST API, xử lý tính toán phức tạp hoặc đẩy message vào queue/broker. Đây là Integration Point giữa engine và các Spring Boot services.

Một chỗ hay vấp khi nối delegate với Spring: khai báo activiti:class thì engine tự new class đó bằng reflection, không đi qua Spring container. Delegate dùng constructor injection như ví dụ dưới sẽ lỗi lúc khởi tạo (không có constructor rỗng), còn field injection thì nhận null. Muốn dùng Spring bean thì khai báo activiti:delegateExpression="${creditScoreDelegate}" (trỏ tới tên bean) hoặc activiti:expression.

Lợi ích lớn của thiết kế này là làm việc song song (Parallel Collaboration). Trong các enterprise workflow, BA cứ vẽ diagram để dựng luồng nghiệp vụ, trong khi Backend Developer độc lập implement các JavaDelegate map thẳng vào Spring Beans. Không ai phải chờ ai, và đó cũng là cách áp dụng "Clean Architecture" vào quy trình làm việc.

# task types

Task TypeIconMô tảImplementation
User Task👤Con người thực hiện (form, approval)Assigned to user/group
Service Task⚙️Hệ thống tự động thực hiệnJava class, REST call
Script Task📜Execute script (Groovy, JS)Inline code
Send Task✉️Gửi messageMessage broker, email
Receive Task📨Chờ nhận messageWait state
Business Rule Task📋Execute business rulesDMN decision table
Manual Task✋Task ngoài hệ thống (physical)No system interaction
Call Activity↗️Gọi process khác (reusable)Sub-process reference

# user task - human interaction

<userTask id="approveRequest" name="Approve Loan Request"
         activiti:assignee="${applicant}"
         activiti:candidateGroups="loan-approvers">
   <documentation>Review loan application and approve/reject</documentation>
 
   <!-- Form properties (hiển thị form cho user) -->
   <extensionElements>
       <activiti:formProperty id="decision" name="Decision"
           type="enum" required="true">
           <activiti:value id="approve" name="Approve"/>
           <activiti:value id="reject" name="Reject"/>
       </activiti:formProperty>
       <activiti:formProperty id="comments" name="Comments"
           type="string" required="false"/>
   </extensionElements>
</userTask>

# service task - automated work

<!-- Java Delegate: engine tự new class bằng reflection, KHÔNG inject Spring bean -->
<serviceTask id="calculateScore" name="Calculate Credit Score"
            activiti:class="com.example.bpm.delegate.CreditScoreDelegate"/>
 
<!-- Expression (call Spring bean method) -->
<serviceTask id="sendNotification" name="Send Email"
            activiti:expression="${notificationService.sendEmail(execution)}"/>
 
<!-- Delegate Expression (Spring bean) -->
<serviceTask id="processPayment" name="Process Payment"
            activiti:delegateExpression="${paymentDelegate}"/>
 
<!-- HTTP Task (REST call): chỉ có ở Flowable, Activiti không hỗ trợ type="http" -->
<serviceTask id="callExternalApi" name="Verify Identity"
            flowable:type="http">
   <extensionElements>
       <flowable:field name="requestMethod" stringValue="POST"/>
       <flowable:field name="requestUrl"
           stringValue="http://identity-service/api/v1/verify"/>
       <flowable:field name="requestBody"
           expression="${execution.getVariable('requestPayload')}"/>
   </extensionElements>
</serviceTask>

# java delegate implementation

@Component("creditScoreDelegate")
@RequiredArgsConstructor
@Slf4j
public class CreditScoreDelegate implements JavaDelegate {
 
   private final CreditScoringService scoringService;
 
   @Override
   public void execute(DelegateExecution execution) {
       String customerId = (String) execution.getVariable("customerId");
       BigDecimal loanAmount = (BigDecimal) execution.getVariable("loanAmount");
 
       log.info("Calculating credit score | customerId={} | loanAmount={}",
           customerId, loanAmount);
 
       CreditScore score = scoringService.calculate(customerId, loanAmount);
 
       // Set process variables cho downstream tasks
       execution.setVariable("creditScore", score.getValue());
       execution.setVariable("riskLevel", score.getRiskLevel().name());
       execution.setVariable("autoApproved", score.getValue() >= 750);
   }
}

# sub-process - embedded process logic

<!-- Embedded Sub-Process: nhóm related tasks, shared error handling -->
<subProcess id="paymentSubProcess" name="Payment Processing">
   <startEvent id="payStart"/>
   <serviceTask id="validatePayment" name="Validate"/>
   <serviceTask id="executePayment" name="Execute"/>
   <serviceTask id="confirmPayment" name="Confirm"/>
   <endEvent id="payEnd"/>
 
   <!-- Sequence flows within sub-process -->
   <sequenceFlow sourceRef="payStart" targetRef="validatePayment"/>
   <sequenceFlow sourceRef="validatePayment" targetRef="executePayment"/>
   <sequenceFlow sourceRef="executePayment" targetRef="confirmPayment"/>
   <sequenceFlow sourceRef="confirmPayment" targetRef="payEnd"/>
</subProcess>
 
<!-- Error boundary on entire sub-process (khai báo ngang hàng, bên ngoài subProcess) -->
<boundaryEvent id="paymentError" attachedToRef="paymentSubProcess">
   <errorEventDefinition errorRef="paymentFailedError"/>
</boundaryEvent>

# multi-instance - loop/parallel execution

<!-- Sequential: process items one by one -->
<serviceTask id="processItem" name="Process Item"
            activiti:delegateExpression="${itemProcessor}">
   <multiInstanceLoopCharacteristics isSequential="true"
       activiti:collection="${orderItems}"
       activiti:elementVariable="currentItem"/>
</serviceTask>
 
<!-- Parallel: process ALL items simultaneously -->
<userTask id="approveByAll" name="Approve By All Managers"
         activiti:candidateGroups="managers">
   <multiInstanceLoopCharacteristics isSequential="false"
       activiti:collection="${approverList}"
       activiti:elementVariable="approver">
       <!-- Complete when 60% approved -->
       <completionCondition>${nrOfCompletedInstances / nrOfInstances >= 0.6}</completionCondition>
   </multiInstanceLoopCharacteristics>
</userTask>

# gateways - "quyết định & hợp nhất"

Nếu coi quy trình là mạng lưới giao thông thì Gateway là các "ngã tư" quyết định hướng đi. Trong BPMN, thứ dùng để theo dõi trạng thái là Execution Token. Khi một token đến Gateway, engine phải tính: rẽ nhánh nào? Có sinh ra các luồng song song không? Khi nào các luồng đó cần hợp nhất (synchronize) để đi tiếp?

Gateway luôn là hình kim cương (◇), icon bên trong cho biết logic định tuyến:

  • Exclusive (XOR - không icon hoặc dấu X): hoạt động như if-else trong code. Token chỉ đi vào DUY NHẤT một nhánh, nhánh đầu tiên thỏa điều kiện.

  • Parallel (AND - dấu +): xử lý đồng thời (Concurrency). Nó nhân bản (fork) token và đẩy vào TẤT CẢ các nhánh cùng lúc. Ở điểm merge (join), hệ thống block cho đến khi TẤT CẢ các nhánh hoàn thành.

  • Inclusive (OR - chữ O): kết hợp hai kiểu trên, token đi vào MỘT hoặc NHIỀU nhánh tùy các nhánh có thỏa điều kiện (conditions) hay không.

Một lỗi nặng liên quan đến quản lý trạng thái: bạn dùng Parallel Gateway để chia (split) luồng thành 3 nhánh song song, nhưng chỗ các nhánh gặp lại nhau lại không dùng một Parallel Gateway khác để gom (merge), mà nối thẳng sequence flow hoặc dùng Exclusive Gateway. Engine không chờ ai cả: mỗi token đến nơi là đi tiếp luôn, nên mọi task phía sau điểm gặp nhau bị chạy 3 lần. Dưới backend, việc này thành 3 email, 3 lần cập nhật cùng một bản ghi (duplicate events) và dễ sinh Race Conditions khi cập nhật database.

Best Practice: chuẩn BPMN cho phép một Gateway vừa split vừa merge (mixed gateway), nhưng theo Clean Design nên tách riêng: 1 Gateway để rẽ nhánh (Fork), 1 Gateway để hợp nhất (Join). Tách như vậy giúp luồng minh bạch, dễ đọc và đỡ mất nhiều giờ debug.

# exclusive gateway (xor) - chọn 1 trong n paths

Nhìn từ góc độ code, Exclusive Gateway (XOR) giống cấu trúc if-else if-else hoặc switch-case.

Đúng như tên gọi "độc quyền" (exclusive), khi Execution Token vào ngã tư này, BPMN Engine đánh giá lần lượt các điều kiện (conditions). Token được đẩy vào DUY NHẤT nhánh đầu tiên thỏa (true) và bỏ qua các nhánh còn lại.

Giống như khối if-else luôn nên có nhánh else cuối cùng, XOR Gateway cần cấu hình một Default Flow (nhánh mặc định). Nếu Token đến Gateway, mọi điều kiện đều trả về false mà không có nhánh default nào hứng, BPMN Engine sẽ ném exception (Activiti báo kiểu "No outgoing sequence flow of the exclusive gateway ... could be selected"). Transaction bị rollback, instance kẹt lại ở wait state trước đó cho đến khi có người sửa dữ liệu hoặc sửa diagram.

<!-- default trỏ tới id của sequence flow mặc định -->
<exclusiveGateway id="decisionGateway" name="Approve or Reject?" default="defaultFlow"/>
 
<sequenceFlow sourceRef="decisionGateway" targetRef="approvedPath">
   <conditionExpression xsi:type="tFormalExpression">
       ${decision == 'approve'}
   </conditionExpression>
</sequenceFlow>
 
<sequenceFlow sourceRef="decisionGateway" targetRef="rejectedPath">
   <conditionExpression xsi:type="tFormalExpression">
       ${decision == 'reject'}
   </conditionExpression>
</sequenceFlow>
 
<!-- Default flow (khi không condition nào match) -->
<sequenceFlow sourceRef="decisionGateway" targetRef="manualReview"
             id="defaultFlow"/>

# parallel gateway (and) - chạy tất cả paths đồng thời

Parallel Gateway (ký hiệu dấu +) là cơ chế xử lý đồng thời (Concurrency) của BPMN. Nó bỏ qua mọi điều kiện (conditions) và chỉ có một mục tiêu: rút ngắn thời gian thực thi bằng cách chạy các tác vụ song song.

Gateway này vận hành qua hai pha:

  • Pha rẽ nhánh (Fork/Split): khi Execution Token chạm điểm Fork, BPMN Engine nhân bản (duplicate) token và đẩy vào TẤT CẢ các nhánh đi ra. Các luồng execution lúc này độc lập với nhau, cho phép trigger nhiều backend services chạy cùng lúc.

  • Pha hợp nhất (Join/Merge): đây là phần quan trọng nhất của pattern này. Tại điểm Join, Gateway đóng vai trò rào cản đồng bộ (Barrier Synchronization). Nó block luồng tổng thể, giữ quy trình ở trạng thái chờ (wait state) cho đến khi TẤT CẢ các nhánh trước đó đều báo hoàn thành. Khi token cuối cùng đến nơi, Engine gom chúng lại thành một token duy nhất để đi tiếp.

<!-- Fork: split into parallel paths -->
<parallelGateway id="forkGateway"/>
<sequenceFlow sourceRef="forkGateway" targetRef="sendEmail"/>
<sequenceFlow sourceRef="forkGateway" targetRef="updateInventory"/>
<sequenceFlow sourceRef="forkGateway" targetRef="generateInvoice"/>
 
<!-- Tất cả 3 tasks chạy ĐỒNG THỜI -->
 
<!-- Join: wait for ALL to complete -->
<parallelGateway id="joinGateway"/>
<sequenceFlow sourceRef="sendEmail" targetRef="joinGateway"/>
<sequenceFlow sourceRef="updateInventory" targetRef="joinGateway"/>
<sequenceFlow sourceRef="generateInvoice" targetRef="joinGateway"/>
 
<!-- Chỉ tiếp tục khi cả 3 xong -->
<sequenceFlow sourceRef="joinGateway" targetRef="completeOrder"/>

# inclusive gateway (or) - chạy 1 hoặc nhiều paths

Inclusive Gateway (ký hiệu chữ O) nằm giữa tính độc quyền (XOR) và tính song song hoàn toàn (AND), dùng cho trường hợp: cần rẽ nhánh theo điều kiện nhưng không giới hạn ở một kết quả.

  • Pha rẽ nhánh (Fork): khác với XOR dừng ngay khi gặp điều kiện đúng đầu tiên, Inclusive Gateway đánh giá (evaluate) conditions ở mọi nhánh. Nhánh nào trả về true đều được cấp một Execution Token để chạy song song. Lúc runtime, có thể kích hoạt 1 nhánh, 3 nhánh, hoặc TẤT CẢ tùy dữ liệu.

  • Pha hợp nhất (Merge): phần này làm Engine đau đầu nhất. Khác với AND Gateway (đợi TẤT CẢ), điểm Join của Inclusive Gateway không biết trước có bao nhiêu nhánh được kích hoạt. Mỗi khi một token đến, engine (như Activiti) kiểm tra xem trong process instance còn execution nào đang chạy mà vẫn có thể đi tới gateway này không. Còn thì chờ tiếp, hết thì gộp luồng và đi tiếp. Linh hoạt hơn nên chi phí quản lý state dưới backend cũng nặng hơn, và với diagram có vòng lặp thì kết quả đôi khi khó đoán.

Kinh nghiệm: giống XOR, Inclusive Gateway vẫn có rủi ro Runtime Exception nếu lúc phân nhánh không có điều kiện nào trả về true. Nhớ thiết lập Default Flow (nhánh mặc định).

<inclusiveGateway id="notifyGateway" name="Notification Channels"/>
 
<!-- Chạy tất cả paths có condition true -->
<sequenceFlow sourceRef="notifyGateway" targetRef="sendSms">
   <conditionExpression>${customer.phoneVerified == true}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="notifyGateway" targetRef="sendEmail">
   <conditionExpression>${customer.email != null}</conditionExpression>
</sequenceFlow>
<sequenceFlow sourceRef="notifyGateway" targetRef="pushNotification">
   <conditionExpression>${customer.deviceToken != null}</conditionExpression>
</sequenceFlow>
 
<!-- Merge: chờ tất cả activated paths hoàn thành -->
<inclusiveGateway id="notifyJoin"/>

# event-based gateway - chờ event nào đến trước

Khác với XOR, AND hay OR (rẽ nhánh dựa trên Data/Condition), Event-Based Gateway định tuyến quy trình hoàn toàn dựa trên các sự kiện bên ngoài (External Events).

Khi Execution Token đến Gateway này, luồng thực thi bị "block" (đóng băng trạng thái). BPMN Engine tạo một loạt "listeners" (lắng nghe sự kiện) tương ứng với các nhánh đi ra, thường là Message Event (chờ webhook) hoặc Timer Event (chờ timeout).

Nguyên tắc ở đây là "đến trước thì được" (First come, first served). Hệ thống giữ trạng thái chờ đến khi sự kiện ĐẦU TIÊN xảy ra. Ngay lúc đó, luồng rẽ vào nhánh của sự kiện thắng, đồng thời hủy (cancel/teardown) ngay toàn bộ listeners ở các nhánh còn lại để giải phóng tài nguyên.

Ví dụ: gửi OTP cho user. Quy trình dừng tại Event-Based Gateway để chờ 1 trong 2 sự kiện: (1) nhận Message "User nhập đúng OTP" → đi tiếp luồng xác thực, HOẶC (2) Timer "Quá 5 phút" → rẽ sang luồng expire OTP. Sự kiện nào đến trước thì nhánh đó chạy.

<!-- Chờ: hoặc customer respond, hoặc timeout 7 ngày -->
<eventBasedGateway id="waitGateway"/>
 
<sequenceFlow sourceRef="waitGateway" targetRef="customerResponse"/>
<intermediateCatchEvent id="customerResponse">
   <messageEventDefinition messageRef="responseMessage"/>
</intermediateCatchEvent>
 
<sequenceFlow sourceRef="waitGateway" targetRef="timeout"/>
<intermediateCatchEvent id="timeout">
   <timerEventDefinition>
       <timeDuration>P7D</timeDuration>
   </timerEventDefinition>
</intermediateCatchEvent>
<!-- Event nào đến TRƯỚC → đi path đó, cancel paths còn lại -->

# process variables & expressions

Nếu cấu trúc BPMN là bộ khung logic thì process variables là "bộ nhớ" (memory) của hệ thống. Mỗi process instance khi khởi tạo có một context variables riêng, lưu trạng thái xuyên suốt vòng đời quy trình, giống HTTP Session trong web application. Các biến này luân chuyển data giữa các node: ví dụ Task A tính toán và set creditScore = 750, Gateway B ngay sau đó đọc biểu thức ${creditScore >= 700} để rẽ nhánh phê duyệt (approve).

Cái bẫy rất hay gặp là lạm dụng process variables. Khác với biến trong RAM, mọi variable trong BPMN engine đều được serialized và persist thẳng xuống Database. Mỗi lần set/update variable là một câu INSERT/UPDATE được thực thi.

Best Practice: áp dụng Claim Check Pattern. Đừng store payload lớn (file content, JSON list khổng lồ) vào biến quy trình, vì sẽ làm phình DB engine và nghẽn I/O. Hãy lưu large data object ở Document Store (như MongoDB) và chỉ truyền Document ID (reference) giữa các node trong BPMN.

Trạng thái trong BPMN có phân cấp, và luật khác nhau tùy loại sub-process:

  • Embedded Sub-Process chạy chung process instance: đọc được biến của parent. Gọi setVariable với tên biến chưa tồn tại thì Activiti tạo biến ở scope cao nhất (process instance), nên parent vẫn thấy. Muốn biến chỉ sống trong sub-process thì dùng setVariableLocal.
  • Call Activity khởi tạo một process instance riêng: mặc định không thấy biến của parent và parent cũng không thấy biến của nó, trừ khi bạn khai báo mapping activiti:in / activiti:out.

Nắm rõ scoping rule này sẽ đỡ cho bạn nhiều giờ debug lỗi "variable not found" lúc runtime.

Các engine (như Activiti/Camunda) đánh giá điều kiện qua UEL (cú pháp ${...} giống JSP EL). Điểm hay khi tích hợp với hệ sinh thái Java là UEL kết nối trực tiếp với Spring ApplicationContext, nên bạn có thể gọi thẳng method của Spring Bean ngay trên diagram, ví dụ ${orderService.calculateTotal(orderId)}, giúp bind logic gọn và decoupling.

// Set variable khi start process
Map<String, Object> variables = new HashMap<>();
variables.put("applicantName", "Nguyen Van A");
variables.put("loanAmount", new BigDecimal("500000000"));
variables.put("applicationDate", LocalDate.now());
variables.put("documents", documentList);
 
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
   "loan-approval", businessKey, variables);
 
// Read/write trong Java Delegate
@Override
public void execute(DelegateExecution execution) {
   BigDecimal amount = (BigDecimal) execution.getVariable("loanAmount");
   execution.setVariable("interestRate", calculateRate(amount));
 
   // Transient variable (không persist, chỉ tồn tại trong memory)
   execution.setTransientVariable("tempCalculation", intermediateResult);
}
 
// Expression trong BPMN XML
// UEL (Unified Expression Language)
${applicantName}                          // Read variable
${loanAmount > 1000000000}                // Condition
${riskLevel == 'HIGH' ? 'manual' : 'auto'} // Ternary
${orderService.calculateTotal(orderId)}    // Call Spring bean method
#{execution.getVariable('status')}         // Alternative syntax

# listeners & lifecycle hooks

Nếu đã quen với Aspect-Oriented Programming (AOP) hay @EventListener trong Spring Boot, thì Listeners là khái niệm tương đương trong BPMN Engine. Chúng là các "event hooks", cho phép inject code vào những điểm cụ thể trong vòng đời quy trình: lúc process vừa khởi tạo, khi token đi qua một sequence flow, hay khi một task đổi trạng thái.

Tại sao dùng Listeners mà không nhét thẳng code vào Java Delegates?

Vì bài toán Cross-cutting Concerns (logic cắt ngang). Những việc như Audit logging (lưu vết), Metrics collection (thu thập chỉ số) hay Notification dispatch (gửi thông báo) không thuộc logic nghiệp vụ chính. Bạn không muốn mọi class Delegate đều dính những dòng lặp đi lặp lại như auditService.log(). Thay vào đó, ta đặt một Listener đứng riêng, "lắng nghe" thay đổi trạng thái từ Engine và xử lý các side-effects này tự động, trong suốt (transparent) với luồng nghiệp vụ.

Có 2 loại chính:

  • Execution Listener (mức Engine): hook sâu vào luồng thực thi của process (start/end của cả process, các activity, hoặc các sequence flow).
  • Task Listener (mức tương tác): hook trực tiếp vào vòng đời của User Task (các sự kiện create, assign, complete, delete).

Các use case hay gặp ở Production:

  • Event-driven notifications: bắn sự kiện (qua Kafka/RabbitMQ) hoặc WebSocket để push realtime notification khi task được assign cho user.

  • Compliance & audit trail: tự động ghi log mọi thay đổi trạng thái để truy vết (ví dụ đẩy log về ELK stack).

  • SLA & timeout management: khởi tạo bộ đếm thời gian (SLA timer) ngay khi task được tạo để đánh giá KPI hoặc trigger luồng escalate.

  • Smart routing: auto-assign công việc theo thuật toán Workload Balancing thay vì gán cứng (hardcode) người xử lý.

# execution listeners - hook vào process flow

<userTask id="approveTask" name="Approve">
   <extensionElements>
       <!-- Chạy khi task START -->
       <activiti:executionListener event="start"
           delegateExpression="${taskAuditListener}"/>
       <!-- Chạy khi task END -->
       <activiti:executionListener event="end"
           expression="${metricsService.recordTaskCompletion(execution)}"/>
   </extensionElements>
</userTask>
 
<!-- Process-level listener -->
<process id="loanProcess">
   <extensionElements>
       <activiti:executionListener event="start"
           delegateExpression="${processStartListener}"/>
       <activiti:executionListener event="end"
           delegateExpression="${processEndListener}"/>
   </extensionElements>
</process>

# task listeners - specific to user tasks

Trong hệ thống Enterprise, User Task (tác vụ người dùng) là điểm nghẽn khó đoán nhất vì phụ thuộc vào tốc độ xử lý của con người. Để quản lý rủi ro này, BPMN có Task Listeners: các hooks để "bắt" (intercept) 4 trạng thái trong vòng đời của một tác vụ:

  • create (khởi tạo): task vừa được tạo trong database nhưng chưa giao cho ai. Đây là chỗ hợp lý để tính và set SLA (VD: tính Deadline Date theo lịch làm việc) hoặc trigger luồng init dữ liệu form từ external APIs.
  • assignment (giao việc): lúc task có chủ (Assignee). Thay vì gán cứng (hardcode) user trên diagram, các Architect thường dùng hook này để nhúng thuật toán Auto-routing/Round-robin: tự query database xem nhân viên nào đang rảnh nhất (lowest workload) để assign, đồng thời bắn Kafka message để đẩy Push Notification đến App của nhân viên đó.
  • complete (hoàn thành): người dùng vừa bấm Submit trên UI. Đừng vội để Engine đẩy luồng đi tiếp. Hook này là chốt chặn cuối (Last Line of Defense) để Backend chạy Custom Validation, hoặc map/transform dữ liệu từ UI form vào Process Variables an toàn trước khi chuyển trạng thái.
  • delete (hủy bỏ): task bị kill giữa chừng (thường do Timer Event hết hạn hoặc có Error Event). Hook này dùng để dọn dẹp tài nguyên (cleanup resources) hoặc gửi email báo "Tác vụ đã bị hủy".

Kinh nghiệm: khi implement interface TaskListener trong Spring Boot, cẩn thận với các tác vụ I/O nặng (như gửi Email) ở hook assignment hoặc complete. Nếu chạy đồng bộ (synchronous), người dùng bấm Submit trên UI sẽ bị "treo" chờ email gửi xong. Nên chuyển sang bất đồng bộ (Asynchronous/Message Queue) ở các điểm này.

<userTask id="reviewTask">
   <extensionElements>
       <!-- Khi task được CREATE (assigned to someone) -->
       <activiti:taskListener event="create"
           delegateExpression="${taskAssignmentListener}"/>
       <!-- Khi task COMPLETE -->
       <activiti:taskListener event="complete"
           expression="${notificationService.notifyCompletion(task)}"/>
       <!-- Khi task bị DELETE (cancelled) -->
       <activiti:taskListener event="delete"
           delegateExpression="${taskCancellationListener}"/>
   </extensionElements>
</userTask>
@Component("taskAssignmentListener")
@RequiredArgsConstructor
@Slf4j
public class TaskAssignmentListener implements TaskListener {
 
   private final NotificationService notificationService;
 
   @Override
   public void notify(DelegateTask task) {
       String assignee = task.getAssignee();
       String taskName = task.getName();
       String processId = task.getProcessInstanceId();
 
       log.info("Task assigned | task={} | assignee={} | processId={}",
           taskName, assignee, processId);
 
       notificationService.sendTaskNotification(assignee,
           "New task assigned: " + taskName);
   }
}

# error handling in bpmn

Một "anti-pattern" nặng mà khá nhiều team mắc khi làm với BPMN là cách xử lý ngoại lệ: hoặc mặc kệ cho hệ thống "crash", hoặc bọc try-catch bên trong Java Delegate rồi "nuốt" (swallow) luôn exception. Cách này làm mất giá trị của Workflow Engine.

BPMN có cơ chế xử lý lỗi tốt hơn nhiều: Boundary Error Event. Thay vì giấu các luồng rẽ nhánh trong code, chỉ cần "gắn" (attach) một sự kiện lỗi trực tiếp lên viền của Task. Khi Java Delegate ném BpmnError, Execution Token bị ngắt khỏi luồng chính (normal flow) và rẽ sang nhánh xử lý lỗi (error path). Như vậy try-catch được nâng từ cấp code lên cấp kiến trúc (diagram level), nơi cả Business Analyst và Developer đều thấy rõ logic xử lý rủi ro.

Điều mọi Backend Engineer cần nhớ: Boundary Error Event CHỈ bắt BpmnError. (Các loại boundary event khác như timer, message, signal thì có trigger riêng.) Đây không phải thiếu sót mà là chủ đích thiết kế (design choice):

  • BpmnError (lỗi nghiệp vụ): ngoại lệ dự đoán được (VD: tài khoản không đủ số dư, vượt hạn mức). Hệ thống chủ động catch và rẽ nhánh.
  • Các Exceptions khác (NullPointerException, RuntimeException): lỗi hệ thống hoặc bug logic ngoài ý muốn. BPMN Engine sẽ KHÔNG catch chúng bằng boundary event. Transaction bị rollback về wait state gần nhất: nếu task chạy đồng bộ thì exception văng ngược về chỗ gọi API (vd taskService.complete); nếu là async job thì engine retry (mặc định 3 lần) rồi chuyển sang dead-letter job, cần team vận hành can thiệp thủ công (manual intervention) hoặc dev vào sửa bug. Camunda gọi trạng thái này là incident. Đừng dùng luồng nghiệp vụ để che lỗi hệ thống.

Kiến trúc phân tán & compensation (saga pattern)

Với quy trình lớn đi qua nhiều microservices độc lập (VD: Giữ chỗ kho → Trừ tiền thẻ → Lên đơn vận chuyển), nếu bước cuối thất bại thì không thể dùng database transaction (ACID) thuần để rollback toàn bộ. BPMN giải quyết bằng Compensation (luồng hoàn tác), về bản chất là một implementation của SAGA Pattern. Mỗi task đã thành công trước đó được "map" với một Compensation Handler. Khi có lỗi ở cuối luồng, bạn vẽ một Compensation Throw Event trên nhánh xử lý lỗi (engine không tự bù trừ nếu diagram không ném event này). Engine sẽ gọi handler của những task đã hoàn thành trong scope đó để chạy logic bù trừ (undo), đưa dữ liệu về trạng thái nhất quán (Eventual Consistency). Task chưa chạy tới thì không bị compensate.

# boundary error event - catch errors từ task

<serviceTask id="callPayment" name="Process Payment"
            activiti:delegateExpression="${paymentDelegate}"/>
 
<!-- Boundary error event: catch payment failure -->
<boundaryEvent id="paymentFailed" attachedToRef="callPayment">
   <errorEventDefinition errorRef="PAYMENT_FAILED"/>
</boundaryEvent>
 
<!-- Khi paymentDelegate throw BpmnError("PAYMENT_FAILED") -->
<sequenceFlow sourceRef="paymentFailed" targetRef="handlePaymentFailure"/>
<serviceTask id="handlePaymentFailure" name="Handle Payment Failure"
            activiti:delegateExpression="${paymentFailureHandler}"/>
@Component("paymentDelegate")
@RequiredArgsConstructor
public class PaymentDelegate implements JavaDelegate {
 
   private final PaymentService paymentService;
 
   @Override
   public void execute(DelegateExecution execution) {
       try {
           paymentService.process((String) execution.getVariable("paymentId"));
       } catch (PaymentDeclinedException e) {
           // Throw BpmnError → caught by boundary error event
           throw new BpmnError("PAYMENT_FAILED", e.getMessage());
       }
       // Các exceptions KHÔNG phải BpmnError → transaction rollback, job retry (nếu async)
   }
}

# compensation - undo completed work (saga pattern)

<!-- Task đã complete thành công -->
<serviceTask id="reserveInventory" name="Reserve Inventory"
            activiti:delegateExpression="${inventoryReserveDelegate}"/>
 
<!-- Compensation handler: undo reservation nếu process cần rollback -->
<boundaryEvent id="compensateReserve" attachedToRef="reserveInventory">
   <compensateEventDefinition/>
</boundaryEvent>
<serviceTask id="cancelReservation" name="Cancel Reservation"
            isForCompensation="true"
            activiti:delegateExpression="${inventoryReleaseDelegate}"/>
<association sourceRef="compensateReserve" targetRef="cancelReservation"/>
 
<!-- Trigger compensation khi cần undo -->
<intermediateThrowEvent id="undoAll" name="Compensate All">
   <compensateEventDefinition/>
</intermediateThrowEvent>

# activiti integration trong spring boot

Đây là chỗ lý thuyết BPMN thành thứ chạy được. Trong hệ sinh thái Java, Activiti (hay các engine tương tự) khi nhúng vào Spring Boot không chỉ là một thư viện (library), mà hoạt động như một Persistent State Machine (máy trạng thái có lưu trữ).

Khi khởi động, Engine tự parse các file BPMN XML, sinh database schema (trên PostgreSQL, MySQL,...) để track trạng thái quy trình. Khi một process instance được kích hoạt, một "execution token" được sinh ra và chạy dọc theo diagram. Token này dừng lại (suspend) tại các "wait states" (như User Task, Message Catch Event). Nhờ persist state xuống database, ngay cả khi server crash hoặc restart, tiến trình vẫn tiếp tục từ đúng chỗ nó dừng, không mất context (variables).

Trong mạng lưới microservices phức tạp, best-practice là không nhúng Activiti tùy tiện. Thay vào đó, nên dựng một dedicated service (ví dụ: Workflow/Orchestration Service) chuyên host Engine này. Các domain services khác giao tiếp với nó qua REST API hoặc Message Broker (như Kafka/RabbitMQ) để start process hoặc complete tasks. Ngược lại, Engine điều phối logic bằng cách gọi các Java Delegates, chính là các Spring Beans bạn viết và gắn vào Service Tasks.

5 API chính của Activiti: để tương tác với Engine, Spring Boot tự inject 5 core services sau vào Application Context:

  • RepositoryService: quản lý vòng đời tĩnh. Dùng để deploy, versioning và query các process definitions (bản vẽ XML).

  • RuntimeService: phần vận hành. Nơi bạn start process instances, signal events và quản lý Process Variables đang chạy (runtime).

  • TaskService: điểm chạm với người dùng. Xử lý vòng đời của User Task: query danh sách việc cần làm, claim (nhận việc), delegate (ủy quyền) và complete.

  • HistoryService: phục vụ Audit. Truy vấn mọi instance/task đã hoàn thành. Hữu ích khi làm luồng đồng bộ dữ liệu về Data Warehouse cho báo cáo.

  • ManagementService: dành cho DevOps. Quản lý các background jobs, timer, cơ chế retry, và xử lý Dead-letter queues khi có luồng bị kẹt.

# process engine configuration

@Configuration
public class ActivitiConfig {
 
   @Bean
   public SpringProcessEngineConfiguration processEngineConfiguration(
           DataSource dataSource,
           PlatformTransactionManager transactionManager) {
 
       SpringProcessEngineConfiguration config = new SpringProcessEngineConfiguration();
       config.setDataSource(dataSource);
       config.setTransactionManager(transactionManager);
       config.setDatabaseSchemaUpdate("true");
       config.setAsyncExecutorActivate(true);
       config.setHistoryLevel(HistoryLevel.FULL);
       return config;
   }
}

# deploy & execute process

@Service
@RequiredArgsConstructor
@Slf4j
public class ProcessService {
 
   private final RepositoryService repositoryService;
   private final RuntimeService runtimeService;
   private final TaskService taskService;
   private final HistoryService historyService;
 
   // Deploy process definition
   public String deploy(String bpmnXml, String processName) {
       Deployment deployment = repositoryService.createDeployment()
           .name(processName)
           .addString(processName + ".bpmn20.xml", bpmnXml)
           .deploy();
       return deployment.getId();
   }
 
   // Start process instance
   public String startProcess(String processKey, Map<String, Object> variables) {
       ProcessInstance instance = runtimeService.startProcessInstanceByKey(
           processKey, UUID.randomUUID().toString(), variables);
       log.info("Process started | key={} | instanceId={}", processKey, instance.getId());
       return instance.getId();
   }
 
   // Complete user task
   public void completeTask(String taskId, Map<String, Object> variables) {
       taskService.complete(taskId, variables);
   }
 
   // Query active tasks for user
   public List<TaskDTO> getMyTasks(String userId) {
       return taskService.createTaskQuery()
           .taskAssignee(userId)
           .orderByTaskCreateTime().desc()
           .list()
           .stream()
           .map(this::toDTO)
           .toList();
   }
}

# best practices

Sau nhiều năm thiết kế và vận hành các Workflow Engines chịu tải cao, đây là những nguyên tắc mình rút ra để hệ thống không biến thành một mớ hỗn độn.

Tư duy thiết kế quy trình (process design)

  • Giữ đơn giản (keep it simple): đừng nhét cả hệ sinh thái vào một bản vẽ. Nếu process diagram vượt quá 30 elements hoặc không hiển thị trọn trên một màn hình thì nên tách thành các Call Activities hoặc Sub-processes. Quy trình tốt là khi cả BA và Developer chỉ mất 30 giây để nắm luồng chính.

  • Happy path first: mô hình hóa luồng thành công (main flow) trước. Sau đó mới thêm Boundary Errors, Timers và Compensation logic. Cố handle mọi edge-case ngay từ đầu sẽ làm chậm tiến độ thiết kế.

  • Immutable deployments (versioning): đừng sửa process definition khi đang có instances chạy thực tế (running). Mỗi thay đổi nên tạo một version mới. Các instances cũ tiếp tục chạy theo version cũ cho đến khi xong.

Tiêu chuẩn implementation

  • Service Tasks phải Idempotent: trong môi trường phân tán, Engine hoặc Async Executor sẽ có lúc retry các failed tasks. Nếu hàm chargePayment() không idempotent (qua Idempotency Keys), khách hàng sẽ bị trừ tiền hai lần.

  • SLA enforcement (boundary timers): không User Task nào nên rơi vào trạng thái "wait forever". Hãy gắn Timer cho mọi tương tác của con người: quá 48h không duyệt → Escalate lên Manager; quá 7 ngày không phản hồi → Auto-cancel.

  • Đặt async boundaries: với các tác vụ I/O nặng (call external APIs, xử lý file), hãy set activiti:async="true". Nếu bỏ qua, service task chạy ngay trên thread của người gọi (vd request HTTP vừa gọi taskService.complete) và trong cùng một transaction: người dùng phải chờ API bên ngoài trả lời, DB connection bị giữ suốt thời gian đó, và lỗi ở task sau sẽ rollback luôn cả bước trước. Có async, engine commit trước, task chạy sau trên thread pool của Async Executor và được retry khi lỗi.

  • Tối ưu database (claim check pattern): variables của quy trình được persist thẳng vào Relational DB (như PostgreSQL). Đừng store payload khổng lồ (VD: 10MB file content). Hãy lưu large data sang Document Store (như MongoDB) và chỉ truyền Data ID (reference) giữa các node. Variables quá nặng sẽ làm chậm query của cả hệ thống.

Vận hành & hệ thống phân tán (operations & distributed design)

  • Giám sát silent failures: hệ thống không báo lỗi không có nghĩa là đang chạy đúng. Nên thiết lập Dashboard alert cho các "điểm mù": instances bị treo (stuck) quá X ngày, tasks không ai nhận (unassigned) quá 24h, và đặc biệt là theo dõi sát Dead-letter Queue.

  • Native saga cho distributed transactions: khi quy trình đi qua nhiều microservices độc lập (VD: Giữ chỗ kho → Trừ tiền → Vận chuyển), đừng dùng Distributed Transactions (2PC). Dùng BPMN Compensation: mỗi node gắn một Compensation Handler để hoàn tác phần việc đã làm. Đó là SAGA Pattern mà engine hỗ trợ sẵn.

  • Test-driven execution: đừng dừng ở Unit Test từng class Delegate. Dùng ActivitiRule (JUnit 4; với Spring Boot thì @SpringBootTest chạy engine thật trên DB in-memory cũng được) để Integration Test cả luồng: deploy bản vẽ, kích hoạt instance và assert đúng sự dịch chuyển trạng thái (state machine) tại từng node.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.