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: EventStorming: Áp dụng Big Picture (1)
Bài 4: Bài này.
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
--------------
Tiếp tục tường thuật lại buổi EventStorming cho bài toán demo.
Hiện tại, chúng ta đang ở điểm quan trọng của buổi workshop để cùng mọi người đưa ra những thông tin chi tiết hơn và cũng rất quan trọng. Ta đã có Actors, Commands, Events để hình thành pattern "Actors thực hiện Commands tạo ra Events". Làm thế nào để Actors thực hiện những Commands cụ thể? Ta đi tới bước tiếp theo: Read models.
Bước 6. Mô hình đọc
Actor cần thông tin để thực hiện command. Thông tin đó đến tự Read Model – mô hình đọc. Trên bảng trắng EventStorming, ta dùng sticky note hình chữ nhật màu xanh lá cây để đại diện.
Read Model là một góc nhìn về dữ liệu trong domain mà actor sử dụng. Đó có thể là một trong các màn hình của ứng dụng, một report, một thông báo, v.v. Thông thường, mô hình đọc là những thông tin đã được số hoá. Ngược lại, nếu không được số hoá, ví dụ như một báo cáo bằng giấy, nó có thể được coi là một External System.
Cùng đi qua từng nhóm actor-command-event để xem mô hình đọc tương ứng là gì.
- Merchant – CreateProduct – ProductCreated: Thông thường, người dùng sử dụng form tạo mới sản phẩm để nhập các thông tin cần thiết. Do đó, ta không cần mô hình đọc ở đây. Nếu ứng dụng có chức năng tạo mới sản phẩm từ template hoặc sản phẩm nào đó thì đó sẽ là mô hình đọc.
- Merchant – CreateStockReceipt – StockReceiptCreated: Tương tự như khi tạo mới Product. Để giữ cho demo đơn giản, ta sẽ không cần mô hình đọc cho nghiệp vụ này.
- Customer – PlaceOrder – OrderPlaced: Hãy phân tích sâu hơn trường hợp này. Hành vi mua sắm mà ứng dụng đưa ra hiện tại bao gồm ba bước:
- Bước 1: Duyệt qua danh sách sản phẩm. Trên danh sách này, bao gồm nhiều sản phẩm, mỗi sản phẩm chỉ có một số thông tin cơ bản đại diện. Như vậy, ở đây sẽ cần một read model, có tên ProductListView.
- Bước 2: Xem chi tiết sản phẩm và đưa vào giỏ hàng. Tại màn hình này, thông tin của sản phẩm được thể hiện chi tiết, bao gồm cả trạng thái tồn kho có khả dụng hay không, số lượt comments, số đánh giá, v.v. Đây là read model mang tên ProductDetailsView. Người dùng bấm “Thêm vào giỏ hàng”.
- Bước 3: Xem giỏ và đặt hàng. Tại màn hình này, thông tin chi tiết giỏ hàng bao gồm danh mục sản phẩm và tổng số tiền được hiện ra. Đây là read model có tên CartView. Người dùng bấm “Đặt hàng”. Lúc này, màn hình hiện ra thông tin tóm lược của đơn hàng, ta gọi read model này là OrderSummaryView. Do ứng dụng chưa hộ trợ CancelOrder (huỷ đơn hàng) nên read model này là điểm kết của nghiệp vụ đặt hàng. Ta sẽ đặt nó ở ngay sau event OrderPlaced.
Để đơn giản hoá ví dụ, ta đã bỏ qua một số read model khác như thông tin của khách hàng (CustomerProfileView)
- Customer – SubmitReviewRating – ReviewRatingSubmitted: Khách hàng cần vào xem chi tiết một sản phẩm rồi mới thực hiện đánh giá, do đó, ta sử dụng lại ProductDetailsView.
Lúc này bảng trắng của chúng ta có dạng như sau:

Bước 7. External Systems
Bước này bổ sung thông tin hệ thống bên ngoài vào mô hình. Một external system được xác định là bất kỳ hệ thống nào mà không phải là một phần của domain đang được khám phá. Nó có thể thực thi các commands hoặc có thể được thông báo qua các events. External system được biểu diễn bằng các sticky note màu hồng.
Với demo hiện tại, external system có vẻ không tồn tại, tuy nhiên trong thực tế, có thể là hệ thống banking được payment process kết nối, hay một hệ thống gửi tới một phiếu nhập kho, hay một danh sách sản phẩm.
Bước 8. Aggregates
Aggregates là gì? Để biết rõ hơn, bạn có thể xem lại series bài viết DDD ở đây.
Ở giai đoạn Big Picture EventStorming, việc xác định aggregate không phải là trọng tâm. Tuy nhiên, nếu có thể, đây sẽ là những thực thể chính của domain khám phá. Các aggregate được biểu diễn bởi các sticky note hình chữ nhật lớn màu vàng (lớn hơn sticky note đại diện cho actor).
Ta hãy đi qua từng nhóm View Model – Actor – Command – Event để xem có gì không.
- Merchant – CreateProduct – ProductCreated: Thực thể chính ở đây không gì ngoài Product.
- Merchant – CreateStockReceipt – StockReceiptCreated: StockReceipt.
- MonitorLowStockPolicy – MonitorLowStock – LowStockAlertTriggered: Chưa cần xác định, do đây là event phụ trong bối cảnh tồn kho.
- Customer – PlaceOrder – OrderPlaced: Order.
- ProcessInventoryPolicy – ProcessInventory – InventoryProcessed: ProductInventory.
- ProcessPaymentPolicy – ProcessPayment – PaymentProcessed: Payment.
- ConfirmOrderPolicy – ConfirmOrder – OrderConfirmed: Order.
- Customer – SubmitReviewRating – ReviewRatingSubmitted: Chưa cần xác định vì đây là event phụ trong bối cảnh sản phẩm.
Như vậy ta có bức tranh sau:

Bước 9. Xác định các bounded context
Bước cuối cùng trong buổi Big Picture EventStorming này là tìm ra những bounded context mà ở đó, mỗi ranh giới trong số chúng sử dụng một mô hình logic nghiệp vụ tách biệt.
Dấu hiệu xác định rất phong phú, từ ubiquitous languague (UL) cho tới các domain events, cùng các policies và aggregates, v.v. Tất cả những thứ này đều được ghi nhận trên bảng trắng của chúng ta. Hãy lưu ý rằng UL (thuật ngữ nghiệp vụ) được mọi người sử dụng để trao đổi, được ghi nhớ lại vào kho thuật ngữ chung, được biểu diễn trên bảng EventStorming. Các thuật ngữ này là căn cứ mạnh để ta xác định ranh giới nghiệp vụ - các bounded context.
- Một bounded context có tên Catalog là phù hợp cho các nghiệp vụ quản lý sản phẩm cho tới lúc này, bao gồm thêm mới và đánh gía sản phẩm. Sau này, các use case khác chắc chắn được bổ sung, có thể là chỉnh sửa sản phẩm, thay đổi ngưỡng cảnh báo tồn kho, v.v.
- Các UL như stock receipt, low stock, process inventory, product inventory, ... tự nhiên tạo thành nhóm về tồn kho. Hiện tại mô hình logic bao gồm hai aggregate, điều này thật ra không quá quan trọng. Vậy ta có thêm một Inventory bounded context.
- Các UL về đơn hàng xuất hiện khá nhiều và rất dễ thấy khi chúng đều xoay quanh mô hình Order aggregate. Chúng tạo thành Order Management. (xem hình vẽ để biết “chúng” là những ai.)
- Còn lại là Payment. Hành vi hiện tại chỉ gồm ProcessPayment và các event tương ứng. Bạn có thể hình dung tính đối xứng mà ta đã bàn tới, sẽ có thêm hành vi có tên ví dụ như CancelPayment.
Như vậy ta có sự phân chia bounded context như hình sau:

Hãy lưu ý rằng, trước khi chuyển sang bước tiếp theo trong buổi EventStorming, hãy chụp lại hình của bước hiện tại để đảm bảo rằng ta có thể "undo".
Bước 10. Tích hợp các bounded context
Hãy nhớ lại vai trò của bounded context trong DDD. Chúng đảm nhận vai trò bảo vệ sự nhất quán của UL (ngôn ngữ phổ quát) trong một ranh giới nghiệp vụ để giúp chúng ta - những stakeholders – nói chuyện với nhau theo cùng một “ngôn ngữ” nhằm tăng tính dễ hiểu, tăng hiệu quả công việc, và tránh mất mát thông tin nghiệp vụ. Dù gì thì sản phẩm cuối cùng vẫn cứ là sản phẩm mà lập trình viên viết ra. Ngoài ra, bounded context còn giúp chúng ta – những nhà phát triển – mô hình hoá bài toán trong một phạm vi vừa đủ. Ta không thể duy trì được một mô hình duy nhất cho toàn bộ tổ chức – một Enterprise Domain Model – vì thông tin được tổng hợp vào cùng những thực thể từ nhiều nghiệp vụ khác nhau dù cùng trong một tổ chức. Do đó, bounded context giúp chia nhỏ mô hình lớn ra thành nhiều mô hình nhỏ hơn. Có thể có những bounded context có những thực thể với tên giống nhau nhưng chúng là khác nhau về hành vi, dữ liệu nghiệp vụ. Ví dụ, cùng thực thể có tên Vehicle nhưng tại bounded context Asset Management, Vehicle bao gồm thông tin định danh như số xe, biển số, hãng, ngày sản xuất, ngày mua, số km đã chạy, ... nhưng tại bounded context Shipment, cũng là thực thể Vehicle nhưng thông tin lại là tuyến đường đã chạy, lượng tải trọng tại mỗi tuyến, vị trí hiện tại, đang chở những đơn hàng, thùng hàng nào, ...
Mô hình lớn bị chia nhỏ khiến nhu cầu tích hợp xuất hiện, là điều đương nhiên của các mô hình chia để trị. Vậy giờ ta đi tới thiết kế để tích hợp các bounded context mà ta đã khám phá ra. Context Map là biểu diễn của cách tích hợp, phối hợp hoạt động giữa các mô hình logic trong các Bounded Context với nhau để tạo thành một luồng xử lý nghiệp vụ tổng thể.
Tại bước Big Picture, Context Map đại diện cho những luồng dữ liệu chính. Sau này, khi đi vào chi tiết, có thể Context Map cần cập nhật thêm những giao tiếp khác nhưng chỉ nên cho những luồng quan trọng.
Catalog - Order Management
Catalog và Order Management quan hệ với nhau ở luồng đặt hàng. Hành động của khách hàng là: Duyệt qua danh sách sản phẩm, xem chi tiết một sản phẩm, đưa vào giỏ hàng, và thực hiện đặt hàng.
Các bước trước của EventStorming đã xác định luồng xử lý này cần ba read models ProductListView, ProductDetailsView, và CartView. Ta đang đặt cả ba read models này tại Order Management. Nếu vậy, các API cho duyệt sản phẩm và xem chi tiết sản phẩm cần nằm tại Order Management. Điều này khiến Order Management trở thành một điểm tập kết lớn của hệ thống, bởi ngoài thông tin cơ bản về sản phẩm, nó còn cần phải lấy cả thông tin tồn kho khả dụng, các điểm đánh giá, nhận xét, và còn nhiều thứ khác trong tương lai.
Microservices hay DDD là cách tiếp cận phân tán (dù DDD không quy định cách triển khai), và các chức năng, hành vi người dùng nên được chia về đúng nơi chúng thuộc về.
Ở đây, Catalog được thiết kế là nơi tập kết các thông tin xoay quan sản phẩm (đó là cụ thể cho demo này, trong thực tế có thể có những bounded context chuyên biệt khác), ta sẽ đặt các hành động duyệt sản phẩm, xem sản phẩm tại đây.
Khi người dùng thêm sản phẩm vào giỏ hàng, hành động được chuyển sang Order Management, do đó, read model cụ thể là ProductDetailsView đang nằm tại Catalog. Làm rõ thêm hành động này. Thông tin sản phẩm đang nằm tại Catalog, nghiệp vụ xử lý nằm tại Order Management khiến ta cần phát triển thêm một số mô hình phụ.
Khi đưa sản phẩm vào giỏ hàng, Order Management cần kiểm tra tính đúng đắn của sản phẩm (sự tồn tại, trạng thái hoạt động). Theo lẽ thường, nó sẽ gọi API của Catalog. Những lời gọi này cũng không quá tệ và thực tế với những trường hợp đòi hỏi tính chính xác cao, ta cần phải làm vậy. Nhưng thường thì không. Và ở đây, ta sẽ chọn giải pháp tăng tính tự trị của Order Management lên (theo đúng tiêu chí của DDD) bằng cách duy trì trong nó một mô hình Product tương tự trong Catalog nhưng là rút gọn. Qua đó, khi Catalog thay đổi thông tin một sản phẩm, Order Management sẽ tiếp nhận thông tin và thực hiện tương ứng trong mô hình mà nó sở hữu. Liệu mối quan hệ này có phải là ACL?
Mặc dù bên trong Order Management duy trì một Product nội bộ để tăng tính độc lập của nó đối với Catalog nhưng những thông tin trong Product thật ra là phiên bản rút gọn của đối tượng cùng tên. Những gì nó giữ lại vẫn mang nguyên ý nghĩa: ProductId, ProductName, v.v. Sự phụ thuộc do đó tăng lên và biến mối quan hệ trở thành Conformist.
Như vậy ta có điều chỉnh sau trong bảng trắng EventStorming sau khi di chuyển các read models và thêm Product aggregate vào Order Management:

Mô hình tích hợp:

Giải thích: “U” – Upstream chỉ định bounded context thượng nguồn, là nơi thông tin được truyền đi. “D” – Downstream chỉ định hạ nguồn, là nơi tiêu thụ thông tin. Trên hình cho thấy hạ nguồn Order Management “conforms” thượng nguồn Catalog khi sử dụng thông tin Product.
Catalog - Inventory
Tương tự như trên, Inventory hiện đang duy trì mô hình ProductInventory bên trong nó từ thông tin Product tại Catalog. Đây là mối quan hệ Conformist tương tự như trên.

Order Management - Payment
Payment cần gì từ Order Management và ngược lại, Order Management cần gì từ Payment? Với bối cảnh hiện tại, dường như mối quan hệ chính giữa hai bounnded context này được thể hiện khi Order Management thực hiện “place order” và yêu cầu Payment tham gia ở khâu thanh toán.
Để mối quan hệ này hoạt động, Payment cần nhận được thông tin cần thiết từ Order Management. Cũng với bối cảnh này, hoạt động duy nhất là “place order” tức ta có thể nói rằng Payment thực hiện thanh toán một Order nhận được. Vậy Order là thông tin được gửi từ Order Management sang Payment.
Nghe có vẻ như Payment phụ thuộc chặt chẽ vào Order Management và khả năng mối quan hệ này có xu hướng trở thành Conformist? Sự quyết định tuỳ thuộc vào chúng ta, người thiết kế. Nếu muốn nâng cao sự độc lập của Payment đối vối Order Management, ta có thể thiết kế sao cho giao diện của nó tránh phụ thuộc chặt chẽ vào thông tin Order. Một ví dụ về API của Payment có thể là
{
order_id: 1234,
amount: 250000,
currency: VND,
customer: 1000
}
Payment thực sự không cần biết tới bên trong Order có những sản phẩm nào, tình trạng tồn kho ra sao hay địa chỉ giao hàng là gì. Nó chỉ cần quan tâm đến việc “thu tiền” cho một giao dịch cụ thể. Sự độc lập hơn của Payment khiến nó có xu hướng sử dụng ACL.
Ngược lại, Order Management có là hạ nguồn của Payment không? Thông tin xử lý thành công hay thất bại của Payment cần được gửi về Order Management để nó cập nhật trạng thái Order xử lý. Nhưng đây chỉ là một trao đổi phụ. Điểm chính ở giai đoạn Big Picture này là Payment là một bước trong quy trình xử lý đơn hàng tại Order Management.
Do đó, ta có bức tranh tích hợp sau:

Order Management - Inventory
Dường như tại mọi điểm tích hợp bounded context ta luôn có những lựa chọn để phản ánh không chỉ tính phụ thuộc hay độc lập giữa chúng mà còn cả đội ngũ sở hữu chúng.
Từ dạng Cooperation (Partnership và Shared Kernel) đề cao sự cộng tác mà qua đó, nếu có thay đổi thì các bên ngồi lại với nhau và cùng tìm ra giải pháp một cách tích cực cho tới Customer-Supplier theo mô hình nhà cung cấp và bên tiêu thụ. Ở dạng này, lại chia thành những kiểu giao tiếp khác nhau: chiều lòng khách hàng (OHS) tới tuân theo nhà cung cấp (Conformist) hay tự có biện pháp bảo vệ mình trước nhà cung cấp cứng nhắc (ACL). Nếu không thể hợp tác được với nhau thì “đường ai nấy đi” (Separate Ways).
Inventory cần gì từ Order Management? Chắc chắn quan trọng nhất là danh mục sản phẩm Product cùng số lượng trong phiếu đặt hàng. Inventory có lẽ không cần biết OrderItem là gì, người mua là ai, cũng như cả địa chỉ của họ nữa. Tính tự chủ của bounded context này cần đề cao hay thấp do lựa chọn của người thiết kế. Ở đây, ta sẽ áp dụng ACL xem sao.

Một góc nhìn khác về các mối quan hệ
Đọc lại tới đây, tôi thấy cần chia sẻ thêm với bạn một góc nhìn khác về mối quan hệ giữa các bounded context này, đặc biệt là { Order Management, Payment } và { Order Managemetn, Inventory }.
Bên trên, ta đã quyết định rằng Payment và Inventory cần có sự độc lập đáng kể với Order Management, điều này thể hiện bởi ACL - Anticorruption Layer. Điều này cho thấy, nếu Order Management có thay đổi giao kèo (contract) đã thỏa thuận, tôi sẽ không bị ảnh hưởng vì tôi đã bảo vệ bởi một lớp ACL. Pattern này đặt sự quan trọng của Payment và Inventory lên cao, giúp nó trở nên độc lập hơn.
Nhưng hãy thử đặt địa vị ở Order Management. Rõ ràng, Inventory và Payment không xuất hiện trong mô tả nghiệp vụ "đặt hàng" của nó. Chúng phát sinh khi ta đi sâu hơn vào phân tích nghiệp vụ. Và giả sử tiếp, nếu Payment và Inventory vốn đã tồn tại trước đó rồi, trước nghiệp vụ "đặt hàng", và các dịch vụ này khá "cứng" (cứng đầu). Họ đã đưa ra một API tiếp nhận theo cách họ thấy là tổng quát nhất, có thể thanh toán hay trừ tồn cho mọi nghiệp vụ nào cần. Vậy ta, Order Management, cần phải làm gì?
Ta chỉ có thể thích nghi thôi, dù ta đang là upstream - thượng nguồn. Order Management sẽ phải tạo ra event hoặc có cơ chế transform event đó sang định dạng cho phù hợp với API có sẵn của hàng xóm. Nếu các hàng xóm thay đổi API này, tức chúng đòi hỏi Order Management cũng cần thay đổi theo, thì Order Management gần như bắt buộc phải thay đổi. Đây là dạng OHS.
Vậy, lựa chọn pattern nào phụ thuộc lớn vào cách phối hợp giữa các team phụ trách các bounded context.
Tổng hợp buổi Big Picture
Như vậy, phiên hội thảo Big Picture đã đi tới điểm cuối khi mà mọi người đã có được những thông tin cần thiết để hình dung ra trong hệ thống có những thành phần nào, cách phối hợp của chúng ra sao cùng những thông tin như sự kiện, hành động, tác nhân, và các thực thể chính. Tất cả được móc nối với nhau tạo thành bức tranh chung của hệ thống, Context Map.
Phiên Big Picture có thể ngắn, có thể dài tuỳ theo scope nghiệp vụ cũng như tính hiệu quả của nó. Những bước tiến hành vừa qua xem chừng khá trơn chu nhưng đúng là nó đã được nghĩ trước nên thực tế có thể khó khăn hơn.
Ngày nay, EventStorming đang nổi lên như một công cụ giúp khai thác nhanh, hiệu quả trí tuệ tập thể. Một trong những điểm cốt lõi mà nó mang lại là “sự bảo toàn thông tin” khi lan truyền giữa các stakeholders so với phương pháp truyền thống vốn dễ gây sai lệch.
Phương pháp EventStorming không có các quy tắc vận dụng cứng nhắc, ngay chính tác giả Brandolini cũng cho như vậy. Bản thân ông cũng đang viết cuốn sách “Introducing EventStorming” tới giờ vẫn chưa hoàn thiện. Do đó, hãy hiểu tinh thần của phương pháp này mà điều chỉnh cho phù hợp với cách vận hành của tổ chức là điều cực kỳ quan trọng.
Kết quả thu được:

Context Map:
