Series:
Bài 1: Giới thiệu hệ thống demo và quy trình phát triển.
Bài 2: EventStorming: Lý thuyết
Bài 3: Bài này.
Bài 4: EventStorming: Áp dụng Big Picture (2)
Bài 5: Thiết kế kiến trúc (1) - Decisions
Bài 6: Thiết kế kiến trúc (2) - Sơ đồ tĩnh, động, và triển khai
--------------
Sau khi đã cái nhìn tổng quan về quy trình phát triển và cách áp dụng EventStorming workshop ở các bài trước, chắc hẳn bạn còn thấy rất khó hiểu thực sự nó là gì, nó vận hành ra sao và kết quả đạt được là gì phải không? Hé lộ chút: bạn sẽ nhận được toàn sticky notes với các màu sắc khác nhau được dán trên một cuộn giấy dài nếu tấm bảng trắng của bạn không đủ lớn.
Giờ ta sẽ áp dụng EventStorming ở giai đoạn sớm của dự án, bài toán demo đã giới thiệu. Giai đoạn này được gọi là Big Picture EventStorming, ánh xạ vào quy trình phát triển đã đề cập, nó thực hiện ở giai đoạn tổng thể của dự án. Mục tiêu là tạo ra một sự hiểu biết chung về hệ thống cần xây dựng và đưa nó vào giai đoạn thiết kế kiến trúc. Hãy cùng đi qua từng bước và thực hiện.
Bước 1. Khám phá phi cấu trúc
Bất kỳ ai cũng được khuyến khích viết ra những sự kiện vào sticky notes màu cam mà họ nghĩ về hệ thống cần xây dựng. Động từ thì quá khứ đơn được sử dụng.
Đây là một số event mà tôi nhận được và ý nghĩa mà người viết giải thích, chú ý rằng, bảng này tôi viết lại trên đây để dễ theo dõi:
Event | Mô tả |
ProductCreated | Sản phẩm mới được tạo |
ProductCreationFailed | Tạo sản phẩm thất bại |
StockReceiptCreated | Phiếu nhập kho được tạo |
StockReceiptCreationFailed | Tạo phiếu nhập kho thất bại |
InventoryUpdated | Tồn kho sản phẩm có biến động |
LowStockAlertTriggered | Thông báo được gửi khi tồn kho dưới ngưỡng cảnh báo |
ProductBrowsed | Customer xem danh sách hoặc chi tiết sản phẩm. |
ItemAddedToCart | Sản phẩm được thêm vào giỏ hàng. |
CartValidationFailed | Thêm vào giỏ thất bại do hết hàng |
OrderPlaced | Đơn hàng được tạo |
OrderPlaceFailed | Tạo đơn hàng thất bại |
PaymentProcessed | Thanh toán thành công qua cổng thanh toán |
PaymentProcessFailed | Thanh toán thất bại |
InventoryProcessed | Tồn kho được xử lý (trừ tồn) sau khi đặt hàng |
InventoryProcessFailed | Xử lý tồn kho thất bại cho đơn hàng |
OrderConfirmed | Đơn hàng được xác nhận thành công |
OrderFailed | Đơn hàng không thể hoàn tất do lỗi ở khâu nào đó |
ReviewRatingSubmitted | Customer bình chọn số sao |
ReviewCommentSubmitted | Customer cập nhật đánh giá. |
ReviewValidationFailed | Đánh giá không hợp lệ do chứa từ ngữ không chuẩn mực |
ProductRatingUpdated | Số sao trung bình và số lượt đánh giá được cập nhật |
Bảng 1: Danh sách event và mô tả mà tôi có được trong buổi họp
Lúc này, bảng của chúng ta có nhiều sticky notes màu cam được dán lên:

Tạm thời bỏ qua thứ tự event theo thời gian, hãy quan tâm tới lý do cho sự xuất hiện của chúng theo giải thích của những người đã nghĩ ra (ở table bên trên).
Bước 2. Sắp xếp theo thời gian
Sau khi không ai đóng góp thêm event, người điều phối cùng mọi người đánh giá từng event trên bảng và cố gắng sắp xếp nó vào thứ tự phù hợp theo thời gian, tức phù hợp trong quy trình nghiệp vụ.
Theo mô tả từ các nhóm user story, mọi người nhận thấy rằng các sản phẩm nên được tạo trước, sau đó phiếu nhập được tạo để tăng tồn kho. Nếu tạo phiếu tồn kho trước mà không có sản phẩm thì phiếu đó không thể hợp lệ được.
Như vậy, một số event thuộc nhóm product cần đứng trước một số event thuộc nhóm tồn kho. Ở đây nhấn mạnh từ “một số” chứ không hàm ý tất cả.
Sản phẩm được nhập tồn kho rồi thì mới có thể cho khách hàng mua. Do đó, nhóm event liên quan đến order cần đứng sau nhóm tồn kho.
Người dùng đánh giá sản phẩm sau khi họ đã mua hàng. Vậy nhóm này đứng sau mua hàng.
Từ phân tích sơ bộ này, những người tham gia họp đã có bức tranh sắp xếp event như sau:

Đánh giá tính khả dụng của từng event
- ProductCreated: Cần thiết, báo hiệu một product mới được tạo.
- ProductCreationFailed: Cần tạo event này nếu ta có logic thống kê, nghiên cứu trải nghiệm khách hàng (xem các lỗi thường là gì). Scope hiện tại không cần vì tạo product lỗi sẽ được phản hồi ngay cho merchant.
- StockReceiptCreated: Hành động sẽ phản hồi kết quả ngay cho merchant nên có thể không cần sự kiện này. Tuy nhiên, hãy xem xét kỹ hơn. Phiếu nhập kho cuối cùng cũng sẽ làm tăng tồn kho sản phẩm. Nếu SA sử dụng hai mô hình riêng biệt, giả sử StockReceipt cho phiếu nhập và ProductInventory thì tác động tồn kho cần xử lý ở class sau. Mà nguyên tắc DDD không khuyến khích xử lý cập nhật trên hai đối tượng trong cùng một transaction. Do đó, ta có xu hướng chuyển sang cơ chế eventual consistency thay vì gộp hai đối tượng vào làm một (ví ProductInventory còn được dùng trong nhiều use case khác).
Vì thế, với tiêu chí của buổi Big Picture, người bên ngoài chỉ cần quan sát thấy tạo phiếu nhập kho thì tồn sản phẩm cần tăng. Điều này dẫn tới hai quyết định: Sử dụng StockReceiptCreated và loại bỏ InventoryUpdated phía sau. - StockReceiptFailed: Sự kiện này không cần thiết do merchant sẽ được phản hồi ngay và nó cũng không được sử dụng để làm gì cả ở service khác.
- InventoryUpdated: Như đã phân tích, ta sẽ không dùng sự kiện này.
- LowStockAlertTriggered: Cần thiết, được tạo cùng thời điểm với InventoryUpdated nếu đạt ngưỡng.
- ProductBrowsed: Nếu cần hành động để phân tích hành vi khách hàng thì cần. Tại scope này không cần thiết.
- ItemAddedToCart: Sẽ cần thiết nếu dùng để phân tích hành vi khách hàng hoặc kiểm tra tồn kho tại thời điểm thêm vào giỏ. Nhưng ta sẽ để việc kiểm tra tồn kho tại thời điểm đặt hàng nên sự kiện này chưa cần thiết.
- ItemRemovedFromCart: Không cần thiết thời điểm này.
- CartValidationFailed: Như phân tích trên, sự kiện này cũng sẽ không cần thiết do kiểm tra tồn tại thời điểm đặt hàng và không cần phân tích hành vi khách hàng.
- OrderPlaced: Cần thiết. Đặt order ở trạng thái chờ hoàn thành luồng xử lý.
- OrderValidationFailed: Không cần thiết do phản hồi được trả ngay về khách hàng và không cần phân tích hành vi hay phân tích lỗi validation để tăng trải nghiệm khách hàng.
- PaymentProcessed: Cần thiết. Sự kiện này báo hiệu một bước trong quy trình đặt hàng thành công.
- PaymentProcessFailed: Cần thiết. Sự kiện này báo hiệu một bước trong quy trình đặt hàng thất bại.
- InventoryProcessed: Sự kiện này báo hiệu một bước trong quy trình đặt hàng thành công.
- InventoryProcessFailed: Cần thiết. Sự kiện này báo hiệu một bước trong quy trình đặt hàng thất bại.
- OrderConfirmed: Cần thiết. Sự kiện này báo hiệu một đơn hàng đã hoàn toàn thành công.
- OrderFailed: Cần thiết, sự kiện này báo hiệu một đơn hàng tạo mới thất bại.
- ReviewRatingSubmitted: Cần thiết vì sự kiện này cần được tiêu thụ bởi luồng tính trung bình cho sản phẩm đó.
- ReviewCommentSubmitted: Không cần thiết do module đánh giá tính chuẩn mực được tích hợp ngay trong luồng này để đảm bảo phản hồi ngay lập tức.
- ReviewValidationFailed: Vì lý do trên, không cần thiết.
- ProductRatingUpdated: Không cần thiết vì chưa có nghiệp vụ sử dụng.
Sau khi lược bỏ đi những event không khả dụng, ta có bức tranh sau:

Bước 3. Những điểm tiềm ẩn vấn đề
Sau khi có được bức tranh tinh gọn về domain event, mọi người tới bước tiếp theo: Đánh giá những event hay những điểm xử lý mà ở đó có tiềm ẩn vấn đề: tiềm ẩn lỗi, khó xử lý, khó khả thi về kỹ thuật, v.v. Tại mỗi điểm đó, ta đặt một hình thoi màu hồng.
- Hãy cùng rà soát lại từng event.
- ProductCreated: Đây khả năng cao là dễ xử lý. Về cơ bản, ứng dụng cần validate các thông tin trên Product cần tạo là có thể tạo được.
- StockReceiptCreated: Cũng có tiềm năng dễ xử lý. Ứng dụng cần validate thông tin phiếu nhập sau đó cập nhật vào hệ thống đồng thời cập nhật tồn kho.
- LowStockAlertTriggered: Là hành động tiếp theo sau StockReceiptCreated hoặc InventoryProcessed, nếu thấy số tồn kho chạm tới ngưỡng thì sinh ra event này. Do đó, đây là hành động dễ xử lý.
- OrderPlaced: Đây có thể là một điểm tiềm năng khó xử lý, nhất là trong hệ thống hướng sự kiện. Theo kinh nghiệm của những SA tham gia buổi họp, đây là điểm cần lưu ý. Vì vậy, mọi người thống nhất đặt một Pain Point tại đây bằng một sticky note hình thoi màu hồng.
- InventoryProcessed: Hành động này không khó khi tiếp nhận Order và thực hiện hành động xử lý tồn kho.
- InventoryProcessFailed: Tương tự như trên, không khó.
- PaymentProcessed: Cũng không khó xử lý. Điểm chú ý là giao tiếp với bên thứ ba – là hệ thống banking.
- PaymentProcessFailed: Không khó xử lý.
- OrderConfirmed, OrderFailed: Mọi người thấy rằng vấn đề giao tiếp và đồng bộ hóa với các xử lý thanh toán và tồn kho cần đảm báo tính nhất quán cao. Thanh toán thành công (tức đã trừ tiền) thì đơn hàng cần được thành công mà nếu kho không đủ tồn hay thanh toán thất bại, đơn hàng cần bị gán là thất bại.
- ReviewRatingSubmitted: dễ xử lý.
Như vậy, bức tranh event trở thành:

Bước 4. Những sự kiện chính
Sự kiện chính, pivotal event, là sự kiện mà ở đó có một sự thay đổi lớn về bối cảnh và quy trình.
Nhìn vào danh sách các sự kiện ở bước 3, ta có thể thấy có những bối cảnh sau – cũng gần giống các user story mô tả:
• Bối cảnh sản phẩm.
• Bối cảnh tồn kho.
• Bối cảnh thanh toán. (phát sinh so với user story.)
• Bối cảnh đặt hàng.
Vậy, những sự kiện nào đánh dấu hay đại diện điển hình cho một bối cảnh và hơn nữa ở đó, quy trình có sự thay đổi đáng kể?
ProductCreated đại diện cho bối cảnh sản phẩm là phù hợp. Bối cảnh này lấy Product làm trung tâm, vậy sự kiện tạo ra Product là một điển hình trong đó. Thế còn ReviewRatingSubmitted thì sao? Sự kiện này này không thể thực hiện được nếu không có sản phẩm. Do đó, nó không phải là chính.
Tại bối cảnh tồn kho, như ta đã phân tích, hành động tạo phiếu nhập kho StockReceipt tạo ra tồn kho sản phẩm. Ngoài ra, danh sách sản phẩm trong kho không nên làm đại diện vì sản phẩm, như đã mô tả trên, nên là đại diện của bối cảnh sản phẩm. Ở đây, trung tâm là biến động số tồn trong kho của sản phẩm đó. Vì thế, StockReceiptCreated là quan trọng.
Lập luận tương tự cho PaymentProcessed – hành động tiếp nhận sự kiện đơn hàng được tạo và thực hiện thanh toán cho đơn hàng đó, nên là đại diện cho bối cảnh thanh toán.
OrderPlaced, OrderConfirmed, và OrderFailed thì sao? Chắc chắn thuộc bối cảnh đặt hàng nhưng sự kiện nào là đại diện? Bối cảnh đặt hàng sau này có thể mở rộng nhiều logic bên trong, xoay quanh đơn hàng. Vậy ta hãy chọn sự kiện mà từ đó một đơn hàng được tạo. Vậy OrderPlaced là sự kiện đại diện xứng đáng.
Sau khi đã xác định được những sự kiện chính, điều phối viên vẽ một trục gióng thẳng đứng qua sự kiện đó. Kết quả như hình sau:

Bước 5. Hành động kích hoạt và tác nhân
Tại thời điểm này, mọi người trong phòng họp bắt đầu đi sâu hơn vào cơ chế của các quy trình kinh doanh: Mọi sự kiện phải là kết quả của một điều gì đó. Đó có thể là:
- Một hành động được thực hiện bởi một user. Hành động, hay command, được ký hiệu bởi màu xanh dương và user, hay actor, được ký hiệu bởi màu vàng.

- Chúng có thể đến từ một hệ thống bên ngoài (external system). External system được ký hiệu bởi hình chữ nhật lớn hơn màu hồng.

- Chúng có thể là kết quả của dòng thời gian trôi – một cơ chế tự kích hoạt của hệ thống.
- Hoặc, chúng có thể là hệ quả của một event khác trong hệ thống. Lúc này ta có thể hiểu là “bất cứ khi nào một điều gì đó xảy ra, thì một điều gì đó khác cũng sẽ xảy ra”. Điều này không phải lúc nào cũng rõ ràng, ta sẽ đặt một ghi chú màu tím nhạt cho nó. Ta gọi chúng là Policy.
Cùng tiếp tục đi qua từng event mà ta đang có tính tới thời điểm này.
ProductCreated được merchant kích hoạt CreateProduct, có thể sử dụng tên hành động khác nhưng cái tên này mang đủ ý nghĩa trong bối cảnh.
Với StockReceiptCreated được kích hoạt bởi merchant thực hiện hành động CreateStockReceipt.
InventoryProcessed được kích hoạt bởi hành động ProcessInventory một cách tự động bởi một Policy tiếp nhận sự kiện OrderPlaced. Tương tự cho sự kiện InventoryProcessFailed.
LowStockAlertTriggered nên được kích hoạt bởi hành động có tên MonitorLowStock. Nhân tố kích hoạt có thể là gì? Một tiến trình giám sát định kỳ hay một policy nào khác? Nếu để tiến trình giám sát định kỳ, có thể cứ một phút tiến trình này được kích hoạt và tính toán lại số tồn kho của các sản phẩm đã được bán tính từ thời điểm chạy gần nhất. Trong ví dụ này, chúng ta sử dụng cách kích hoạt tự động bởi hai sự kiện StockReceiptCreated và InventoryProcessed. Do đó, ta cần một Policy nữa ở đây. Tại sao không gọi luôn sự kiện này ngay trong hoặc đồng thời với ProcessInventory command? Nếu nắm vững DDD bạn sẽ cân nhắc khi làm vậy.
Tiếp tục (và có phần tẻ nhạt), sự kiện OrderPlaced được kích hoạt bởi hành động PlaceOrder từ actor Customer.
PaymentProcessed, PaymentProcessFailed, OrderConfirmed, và OrderFailed đều được kích hoạt tự động bởi các Policy bắt nguồn từ các sự kiện tương ứng, với đầu nguồn là OrderPlaced.
ReviewRatingSubmitted được kích hoạt bởi command SubmitReviewRating từ actor Customer.
Qua phân tích trên, có thể bạn thấy buồn cười vì cách đặt tên có phần đơn điệu, tẻ nhạt: CreateProduct – ProductCreated, PlaceOrder – OrderPlaced, ProcessInventory – InventoryProcessed, v.v. Đây là điều tốt vì những nhà phát triển hệ thống thường có xu hướng đơn giản hoá những yếu tố nghiệp vụ như vậy. Và, họ còn thường có suy nghĩ đối xứng:
- Có hành động CreateProduct, vậy sẽ cần hành động DeleteProduct?
- Có hành động PlaceOrder, vậy sẽ có hành động CancelOrder?
- Có hành động ProcessInventory, vậy sẽ có hành động CancelInventory?
- V.v.
Và thật sự thì những suy nghĩ này là đúng. Chúng ta sẽ “để dành” những hành động này ở phía sau của demo này để giúp cho bài toàn ở giai đoạn này đơn giản, phù hợp với mục đích demo của nó.
Ta có bức tranh cập nhật như sau:

(Để thuận tiện theo dõi, chúng ta sẽ ngắt bài này ở đây và chuyển sang bài kế tiếp.)