Event là gì?
Một event là một sự kiện gì đó đã xảy ra trong quá khứ.
Khi bạn viết một phần mềm, chung quy lại tất cả mọi thứ chỉ để phục vụ cho mục đích cuối cùng là quản lý các đối tượng có trong hệ thống. Một phần mềm quản lý nhân sự sẽ quản lý danh sách và trạng thái của các nhân sự, một hệ cơ sở dữ liệu quản lý danh mục các bảng và dữ liệu bên trong chúng, một hệ điều hành quản lý các tiến trình và các thiết bị ngoại vi…
Lưu ý là “trạng thái” biểu thị giá trị hiện tại của tất cả các thuộc tính, ví dụ tên hay địa chỉ của nhân sự, giá trị của các dòng trong một bảng hay ID của một tiến trình…
Mỗi một sự thay đổi trạng thái sẽ dẫn đến một sự kiện, hay nói cách khác mỗi một sự kiện đại diện cho một sự thay đổi trạng thái nào đó. Vì sự kiện luôn là một thứ đã diễn ra trong quá khứ nên giá trị của nó không thể thay đổi và được đặt tên ở thì quá khứ, ví dụ như PersonNameChanged, RowAdded hay OrderDelivered. Tất nhiên chúng ta sẽ thấy rằng trong một phần mềm sẽ có rất nhiều sự kiện xảy ra, việc mô hình hóa hay viết ra tất cả các event là không thể, và thực ra là không cần thiết, chúng ta chỉ cần định nghĩa các sự kiện có ảnh hưởng đến bài toán cần giải quyết.
Domain Event
Trước khi tìm hiểu về Domain Event, ta cần tìm hiểu Domain là gì.
Khi bạn giải quyết một bài toán, những gì liên quan đến nghiệp vụ chuyên môn của bài toán đó được gọi là domain. Một phần mềm quản lý giao nhận hàng hóa cần quản lý những gì? Các đơn hàng, quy trình giao và nhận, ghép và tách chuyến, cách quản lý các loại hàng hóa nguy hiểm, hàng lạnh, thời gian lưu kho, các chứng từ sử dụng trên đường vận chuyển, mức tiêu thụ nhiên liệu, mức phát sinh CO2, quản lý vị trí, quy trình xử lý hàng không giao được… Vô số thứ, nhiều đến mức mà nếu không có sự hướng dẫn từ các chuyên gia trong ngành thì bạn sẽ không thể hiểu hết được.
Các chuyên gia trong ngành này còn được gọi là các domain expert.
Có một vấn đề nữa khi chúng ta xây dựng các phần mềm lớn có nghiệp vụ phức tạp, đó là vì có quá nhiều thứ nên sẽ rất khó để quản lý, bảo trì. Nếu các bạn đã từng nghe câu: “Cái này chỉ có đập đi xây lại” thì câu đó có nghĩa là chi phí bảo trì đã trở nên cao hơn cả chi phí xây mới, và điều này không may lại vô cùng phổ biến. Một trong những cách để giảm độ phức tạp của bài toán là chia để trị, tức là tách thành các bài toán nhỏ hơn và xây dựng chúng độc lập hoàn toàn với nhau. Mỗi một bài toán nhỏ này sẽ có thể được xây dựng, triển khai hoàn toàn độc lập, và thậm chí có thể hoạt động dưới dạng các “microservice”, và muốn như vậy ta cần tách chúng thành các “domain con”.
Việc tìm kiếm và chia tách thành các domain con nằm ngoài phạm vi bài viết này.
Tới đây ta sẽ phân biệt các sự kiện xảy ra bên trong một domain con, ta gọi là Domain Event (từ đây trở đi ta dùng từ domain để chỉ domain con) và các sự kiện xảy ra giữa các domain là Integration Event.
Vì sao ta cần sử dụng các domain event? Trong một domain, chúng ta luôn có các ràng buộc nào đó, theo một quy trình nào đó để đảm bảo trạng thái của các đối tượng là “consistent”, tức là chúng luôn phải – một cách kết hợp với nhau – ở một trạng thái đúng đắn nào đó. Chúng ta có thể xem một ví dụ sau:

Trong ví dụ trên, mỗi Product thuộc về một Category, và các Product chỉ được hiển thị hoặc đặt hàng khi thuộc về một Category có trạng thái is_available là true. Để làm điều này tất nhiên ta có thể sử dụng phép JOIN, tuy nhiên nhược điểm của JOIN là chậm bởi với mỗi sản phẩm chúng luôn phải tìm lại Category tương ứng để xét xem is_available có là true hay không. Giải pháp giúp việc query hiệu quả hơn nhiều là đặt ngay thuộc tính is_available trong mỗi sản phẩm, và ta có thể đọc luôn giá trị của nó để biết có cho phép đặt hàng hay không. Để làm điều này ta cần đảm bảo quy tắc: Product.is_available luôn bằng với Category.is_available của phân loại tương ứng, và để đảm bảo quy tắc này, ta sẽ phát ra một sự kiện CategoryAvailabilityChangedEvent, và sau đó định nghĩa một event handler để bắt, event handler này sẽ cập nhật lại is_available của tất cả các Product với giá trị phù hợp.

Một câu hỏi là tại sao khi Category Manager cập nhật trạng thái của Category, nó không cập nhật luôn trạng thái của Product? Chẳng phải như vậy đơn giản hơn hay sao?
Câu trả lời lại cũng là sự đơn giản. Trong các bài toán có nghiệp vụ phức tạp, danh sách các ràng buộc có thể rất dài, cũng như có thể rất phức tạp, và gom việc xử lý toàn bộ vào một nơi sẽ dẫn đến việc khó bảo trì, và thậm chí là khó test nữa. Để test chức năng A, bạn sẽ cần set up mọi thứ B, C, D, E… có liên quan. Trong khi đó với ví dụ ở trên, bạn không cần quan tâm đến Product khi chỉ muốn test trạng thái của Category (vấn đề tương tự xảy ra khi dùng JOIN).
Nhớ là Domain event luôn được xử lý đồng bộ, trong cùng tiến trình, không có bất kỳ cơ chế message queuing nào được sử dụng, do vậy bạn hoàn toàn có thể sử dụng các cơ chế kiểu như database transaction.
Integration Event
Khác với domain event, integration event được phát ra và xử lý bởi các dịch vụ khác nhau, chúng cũng được dùng để thông báo trạng thái của các thực thể có liên quan đến các dịch vụ khác trong cùng hệ thống. Với bài toán Category/Product như trên, khi chúng ta cập nhật lại trạng thái của Product, chúng ta cần thông báo cho các dịch vụ khác, Basket (quản lý giỏ hàng) chẳng hạn, bởi một sản phẩm không còn sẵn dùng cũng cần được xóa khỏi các giỏ hàng. Như vậy, mỗi khi đặt Product.is_available = false, ta cũng cần phát ra một integration event có tên ProductAvailabilityChangedIntegrationEvent, event này sẽ được xử lý bởi các dịch vụ có liên quan.
Vậy ta có cần phát ra một sự kiện CategoryAvailabilityChangedIntegrationEvent mỗi khi trạng thái của Category thay đổi? Có lẽ là không. Ai quan tâm tới Category ngoài Catalog service chứ? Category có lẽ chỉ là một thực thể/khái niệm có giá trị bên trong Catalog service, vậy nên việc phát ra một sự kiện như vậy chẳng mang ý nghĩa gì cả.
Integration Event luôn được chuyển qua các cơ chế bất đồng bộ, thông qua các message broker hoặc các cơ chế tương tự, vì chúng được gửi ra ngoài service nên chúng ta chỉ đơn giản implement chúng theo kiểu fire-and-forget và sẽ không có cách nào, hay chính xác hơn không cần quan tâm event đó được xử lý bởi ai hay như thế nào.
Như vậy với bài toán Category/Product ta sẽ có mô hình hoạt động như trong hình dưới đây.
