Domain Event - phần 1 xem tại đây.
Cơ chế (tiếp)
Thời điểm và địa điểm
Khi nào Domain sinh ra events và tại đâu?
Domain tiếp nhận yêu cầu (command), xử lý logic nghiệp vụ khiến nó thay đổi trạng thái. Sự thay đổi này cần được thông báo qua Domain Events. Vậy khi Application gọi tới Domain và khiến Domain thay đổi trạng thái thì cần tạo ra Domain Events. Không phải mọi sự thay đổi đều cần thông báo ra bên ngoài mà chỉ những thay đổi mà Domain Experts quan tâm. Với kiến trúc Event Sourcing, mọi sự thay đổi đều cần ghi nhận vì với dạng kiến trúc này, Domain Events chính là dữ liệu. Qua đây ta cũng thấy Domain Events cần được Domain sinh ra chứ không phải Application.

Đây là một ví dụ về cách mà Domain Events không được sinh ra bởi Domain:
public class OrderManagementUseCase {
...
public void createOrder(CreateOrderRequest request) {
// Tính toán các giá trị cần thiết để tạo Order
...
Order order = OrderFactory.createOrder(
new OrderId(),
itemInputs,
customerInfo,
paymentInfo,
discountCodes
);
// Tạo Domain Event
OrderCreated orderCreated = new OrderCreated(order);
eventPublisher.publish(orderCreated);
orderRepository.save(order);
}
}
Tạo Domain Events tại Application khiến cho Domain không mang trách nhiệm cần thiết của nó. Sử dụng cách này khi gặp tình huống hãn hữu: Bên cung cấp Domain không sinh Domain Events và bên sử dụng vì thế phải tự tạo. Ví dụ này, tạo Order là dễ dàng quan sát, nhưng ta không thể đảm bảo chắc chắn rằng mọi lời gọi tới Domain mà ta sử dụng có thay đổi trạng thái để mà sinh Events.
Order lấy về từ OrderFactory là valid (theo phân tích từ Factory), vậy nên ngay tại cuối hàm OrderFactory.createOrder() ta nên tạo ngay Event trước khi trả về Order được tạo.
public Order createOrder(OrderId orderId,
List itemInputs,
CustomerInfo customerInfo,
PaymentInfo paymentInfo,
List discountCodes) {
// Tạo danh sách các OrderItems từ OrderItemInputs
List orderItems = itemInputs.stream()
.map(itemInput -> new OrderItem(
new OrderItemId(),
itemInput.getProductId(),
itemInput.getQuantity(),
itemInput.getUnitPrice()))
.collect(Collectors.toList());
Money totalAmount = calculateTotalAmount(orderItems, discountCodes);
Order order = new Order(orderId, orderItems, customerInfo, paymentInfo, discountCodes, totalAmoun);
OrderCreated orderCreated = new OrderCreated(order);
// Tạm thời bỏ qua hành xử với Domain Event này
return order;
}
Trong đoạn code trên, Order mới được tạo ra và sau đó mới tới OrderCreated event. Tạm thời bỏ qua cách xử lý với Domain Event (publish hay return).
Code như trên là hợp lý bởi những điểm sau:
- OrderFactory đảm bảo đúng theo nguyên tắc Factory của DDD: Chứa logic tạo Order phức tạp, Order tạo ra đảm bảo valid, và Order's constructor được đặt là default giúp cho OrderFactory trở thành điểm duy nhất trong Domain có thể tạo mới Order.
- OrderCreated được đóng gói cùng với việc tạo Order giúp cho logic tạo Order và tạo Event trở thành một khối thống nhất không thể tách rời.
Ta cùng thử xem hành vi thay đổi địa chỉ giao của Order xem sao. Đầu tiên, cùng xem cấu trúc của Order Domain:

Hình 2 cho thấy cấu trúc của Order Domain.
- Order là một Aggregate với root là chính nó. Các thuộc tính của Order ánh xạ sang các class khác với tên ở phía kia đường kết nối. Một số hành vi nghiệp vụ của Order là changeStatus, updatePaymentStatus, changeAddress.
- OrderFactory tạo ra Order Aggregate.
- OrderItem là một Entity. Bên trong Order, đây là một danh sách OrderItem.
- Một số Value Objects là thuộc tính của Order, trong đó ta để ý tới CustomerInfo.
- CustomerInfo là một Value Object, chứa thông tin địa chỉ (address: Address), đây chính là địa chỉ giao. (Giả sử vậy với ví dụ này.)
Như vậy, hành động thay đổi địa chỉ giao của Order có thể có hai lựa chọn: Tạo mới CustomerInfo khác hoặc ủy quyền xuống CustomerInfo để mang tính đóng gói nghiệp vụ tốt hơn.
Tạo mới CustomerInfo:
public class Order {
...
public void updateAddress(Address newAddress) {
this.customerInfo = new CustomerInfo(this.customerInfo.getCustomerId(), newAddress, this.customerInfo.getPhone());
}
...
}
Ủy quyền xuống hành động của CustomerInfo:
public final class CustomerInfo implements ValueObject {
private final CustomerId customerId;
private final Address address;
private final Phone phone;
...
public CustomerInfo withUpdatedAddress(Address newAddress) {
return new CustomerInfo(this.customerId, newAddress, this.phone);
}
}
public class Order {
...
public void updateAddress(Address newAddress) {
this.customerInfo = this.customerInfo.withUpdatedAddress(newAddress);
}
...
}
Bởi CustomerInfo là một Value Object, nó cần đảm bảo Immutable, do đó, khi cập nhật, ta cần trả về một CustomerInfo mới.
Quay trở lại mục đích chính của bài viết này. Với trường hợp cập nhật địa chỉ giao, DeliveryAddressChanged event được sinh ra ở đâu?
Vẫn sử dụng nguyên tắc trạng thái thay đổi ở đâu thì sinh event ở đó. Ta có:
Với trường hợp cập nhật địa chỉ mới bằng cách tạo mới CustomerInfo:
public class Order {
...
public void updateAddress(Address newAddress) {
this.customerInfo = new CustomerInfo(this.customerInfo.getCustomerId(), newAddress, this.customerInfo.getPhone());
DeliveryAddressChanged deliveryAddressChanged = new DeliveryAddressChanged(this.id, this.customerInfo.address());
// Tạm thời bỏ qua hành xử với Domain Event này
}
...
}
Khá rõ ràng với trường hợp này, Domain Event sinh ra tại cuối hành vi nghiệp vụ updateAddress.
Với trường hợp thứ hai, ủy quyền cập nhật xuống CustomerInfo thì sao? Ta sinh event tại đây? Code sẽ trông như thế này:
public final class CustomerInfo implements ValueObject {
private final CustomerId customerId;
private final Address address;
private final Phone phone;
...
public CustomerInfo withUpdatedAddress(Address newAddress, OrderId orderId) {
DeliveryAddressChanged deliveryAddressChanged = new DeliveryAddressChanged(orderId, this.customerInfo.address());
// Tạm thời bỏ qua hành xử với Domain Event này
return new CustomerInfo(this.customerId, newAddress, this.phone);
}
}
Có vấn đề ở đây:
- Giao diện hàm đã phải bổ sung thêm argument OrderId để giúp tạo Event. Điều này khiến CustomerInfo bị gắn chặt hơn với Order và tạo ra mối quan hệ 2 chiều (dù trong một module điều này không quá vấn đề). Nếu Event này cần nhiều hơn thông tin từ Order, ta thậm chí phải truyền vào cả Order.
- Value Object có thể được dùng tại nhiều Aggregate hay Entity khác nhau, hay có thể trong một Value Object khác. Ta mang CustomerInfo với thông tin liên quan tới Order đi theo là rất không hợp lý. Việc bổ sung này khiến cho CustomerInfo khó có khả năng mở rộng.
Vậy ta không nên sinh Domain Event tại Value Object.
Tiếp tục, một sự lựa chọn khác, vẫn ủy quyền thay đổi địa chỉ xuống CustomerInfo nhưng sinh events xảy ra tại Aggregate:
public class Order {
...
public void updateAddress(Address newAddress) {
this.customerInfo = this.customerInfo.withUpdatedAddress(newAddress);
DeliveryAddressChanged deliveryAddressChanged = new DeliveryAddressChanged(orderId, this.customerInfo.address());
// Tạm thời bỏ qua hành xử với Domain Event này
}
...
}
Với lựa chọn này, mọi thứ trông đã tự nhiên hơn và giống với trường hợp không ủy quyền mà tạo mới CustomerInfo tại Order.
Như vậy, ta rút ra một quy tắc: Domain Event được tạo tại nơi mà trạng thái của Aggregate bị thay đổi:
- Tại Factory, nơi Aggregate được tạo ra.
- Tại hành vi nghiệp vụ của Aggregate, nơi nó bị thay đổi trạng thái và phù hợp nhất vì nó là Root.
- Tại Domain Service, nơi hành vi nghiệp vụ cũng được xảy ra nhưng hành vi không phù hợp khi đặt tại Aggregate. Trường hợp này chúng ta sẽ xem xét sau do tới thời điểm này ta chưa bàn về Domain Service.
Giờ ta đã biết sơ bộ về Domain Event nên chứa thông tin gì và khi nào cũng như ở đâu việc sinh sẽ hợp lý. Vẫn có điểm băn khoăn tiếp theo: Cách thức Domain chuyển Events ra bên ngoài, tức tầng Application như thế nào? Cùng đi tới phần tiếp theo: Publish Events.
Publish Events
Chú ý rằng ở đây chưa bàn về cách publish events từ service lên message queue mà chỉ nội bộ trong một service: Cách Domain publish Events.
Xem lại hình 1 bên trên ta thấy rằng Events được sinh ra tại Domain, lần lượt đi qua các tầng Application, sau đó tới Adapter rồi mới được công bố ra bên ngoài qua một message queue. Chỉ cần Application có được Events là Adapter có thể dễ dàng làm tiếp phần việc của mình.
Theo nguyên lý thiết kế CQS (Command-Query Separation), một phương thức thực hiện một hành động làm thay đổi trạng thái của hệ thống thì không nên trả về kết quả, nghĩa là return type là void; còn nếu phương thức chỉ truy vấn thông tin mà không làm thay đổi trạng thái hệ thống nên luôn có kết quả trả về. Nguyên lý thể hiện rõ tính tách biệt giữa Command và Query giúp cho mã nguồn dễ hiểu và dễ bảo trì hơn.
Nếu áp dụng quy tắc này, ta gặp chút rắc rối: Hành động tạo Events chắc chắn làm thay đổi trạng thái hệ thống, nhưng nó cần return void. Làm thế nào để ta có được Events mà nó vừa sinh ra?.
Để tăng tính trừu tượng hóa Domain Event, ta sẽ tạo ra một abstraction là DomainEvent interface để các Events khác kế thừa như sau:

Các Domain Events, ngoài những thông tin chung như id (kiểu EventId), version (kiểu int), occurredAt (kiểu Date) - ghi nhận thời điểm xảy ra sự kiện thì có các thông tin riêng. Với OrderCreated, thông tin riêng là toàn bộ Order vừa được tạo, với DeliveryAddressChanged, thông tin riêng là OrderId bị thay đổi và địa chỉ mới.
Application chỉ quan tâm những Events cần thiết
Đầu tiên cần nhận ra rằng, Domain và Application là hai tầng tách biệt nhau. Domain thì tập trung vào nghiệp vụ còn Application thì sử dụng, phối hợp các Domain để tạo ra các chức năng ứng dụng. Sự tách biệt trách nhiệm này khiến cho ta gặp phải tình huống Domain sinh ra nhiều Events khác nhau nhưng Application chỉ quan tâm tới một tập nhỏ, cần thiết với nó. Vậy ta cần có cơ chế filter để lọc ra Events cần cho Application. Việc này cũng có nhiều cách triển khai khác nhau.
Phá vỡ CQS
Domain behaviors một mặt làm thay đổi trạng thái Domain, một mặt trả về các Events tương ứng, do đó phá vỡ CQS.
Với hành vi cập nhật địa chỉ giao, ta có sự thay đổi hành vi như sau:
public class Order implements Aggregate {
...
public List updateAddress(Address newAddress) {
this.customerInfo = this.customerInfo.withUpdatedAddress(newAddress);
List events = new ArrayList<>();
events.add(new DeliveryAddressChanged(this.id, this.customerInfo.getAddress()));
return events;
}
...
}
Order.updateAddress(...) cho thấy hai việc: cập nhật trạng thái mới của Order (là customerInfo), tạo và trả về danh sách Events (là DeliveryAddressChanged). Các thuộc tính version và occurredAt của Event được khởi tạo ngầm trong constructor của Event (cần xem xét điều này tùy thuộc tình huống cụ thể). Application tiếp nhận và khai thác:
public class OrderManagementUseCase {
...
public void updateDeliveryAddress(OrderId orderId, Address newAddress) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
List events = order.updateAddress(newAddress);
List filteredEvents = filterEvents(events);
eventStore.saveAll(filteredEvents);
}
private List filterEvents(List events) {
return events.stream()
.filter(event -> event.getClass().getCanonicalName().equals(DeliveryAddressChanged.class.getCanonicalName()))
.collect(Collectors.toList());
}
...
}
Code trên cho thấy Application lọc Domain Events quan tâm theo class canonical names bởi nó chính là đại diện vững chắc cho event type thay vì một thuộc tính. Tầng Application lấy về events mong muốn rồi save xuống event store theo Transactional Outbox pattern.
Với OrderFactory sẽ phức tạp hơn một chút vì vốn nó đã trả về Order, giờ cần thêm Events nữa. Để làm điều này, ta có thể dùng Map hoặc khai báo thêm một class nữa tại Domain: Pair - đóng vai trò là lớp tiện ích, như sau:
public class Pair {
private final T1 first;
private final T2 second;
public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}
public T1 getFirst() {
return first;
}
public T2 getSecond() {
return second;
}
}
public class OrderFactory {
public Pair> createOrder(...) {
...
Order order = new Order(orderId, orderItems, customerInfo, paymentInfo, discountCodes, totalAmount);
List events = new ArrayList<>();
events.add(new OrderCreated(order));
// Trả về cả Order và danh sách Domain Events
return Pair.of(order, events);
}
...
}
public class OrderManagementUseCase {
...
public void createOrder(...) {
// Tạo các Value Objects cần thiết rồi gọi OrderFactory
...
Pair> result =
orderFactory.createOrder(orderId, itemInputs, customerInfo, paymentInfo, discountCodes);
Order order = result.getFirst();
List events = result.getSecond();
List filteredEvents = filterEvents(events);
orderRepository.save(order);
eventStore.saveAll(filteredEvents);
}
...
}
Tóm lại, với cách trả về Domain Events tường minh:
- Ưu điểm: Đơn giản, dễ hiểu, trực quan cao. Application gọi tới Domain và nhận được Events trả về trong ngay chính lời gọi.
- Nhược điểm: Vi phạm CQS. Application cần biết các phương thức này ngoài trả về Domain Events thì chúng còn thay đổi trạng thái Domain
Cách thứ hai thường gặp hơn, ta lưu Events trong chính Aggregate và do đó có thể lấy ra khi cần.
Aggregate as Event Container
Ý tưởng này đến từ sự thuận tiện: Events sinh ra vì Aggregate thay đổi trạng thái, vậy mình lưu luôn chúng bên trong chính Aggregate này.
Lúc này, các Aggregate có thêm thuộc tính mới, là danh sách Domain Events:
public class Order implements Aggregate {
private OrderId id;
private List items;
private CustomerInfo customerInfo;
private PaymentInfo paymentInfo;
private List discountCodes;
private Money totalAmount;
private List events = new ArrayList<>();
...
}
Khi tạo mới Order, Events được sinh ra ngay chính trong constructor chứ không cần OrderFactory nữa bởi ta biết rằng một khi Order valid, nó cần phải được tạo tại factory này:
public Order(OrderId id, List items, CustomerInfo customerInfo, PaymentInfo paymentInfo,
List discountCodes, Money totalAmount) {
this.id = id;
this.items = items;
this.customerInfo = customerInfo;
this.paymentInfo = paymentInfo;
this.discountCodes = discountCodes;
this.totalAmount = totalAmount;
this.events.add(new OrderCreated(this));
}
Còn đối với trường hợp thay đổi địa chỉ giao:
public class Order implements Aggregate {
...
public void updateDeliveryAddress(Address newAddress) {
this.customerInfo = this.customerInfo.withUpdatedAddress(newAddress);
this.events.add(new DeliveryAddressChanged(this.id, this.customerInfo.getAddress()));
}
...
}
Các Use cases tinh gọn hơn so với khi vi phạm CQS. Nhưng giờ ta cần đối mặt với vấn đề lớn hơn: Cần làm gì với Events bên trong Aggregate?
Thông thường, Events sinh ra cần được publish ra ngoài theo một trong hai hình thức: publish trực tiếp hoặc thông quan Transactional Outbox như ví dụ trên (eventStore.save...) Hiện tại, Aggregate của ta đang chứa Events vừa sinh, ta cần xử lý thế nào?
Coi Events là thuộc tính
Khi Events là thuộc tính của Aggregate, chúng cần được save xuống database và load từ database lên ứng dụng cùng nhau. Lúc này ta cần mô hình hóa Event dưới dạng Entity hay Value Object? Câu hỏi tiếp tục được đặt ra: Ta có cần lưu toàn bộ Events của toàn vòng đời Aggregate không? Cùng lần lượt trả lời các câu hỏi này, qua đó, ta sẽ thấy rõ hơn bản chất của thiết kế phần mềm: mọi thiết kế đều có mục đích, ý nghĩa nhất định đứng đằng sau.
Events là Entities hay Value Objects?
Nhớ lại rằng, khi coi một thứ là Entity, nó cần có ID và nhờ đó, nó có thể thay đổi thoải mái mà vẫn là chính nó, vẫn có thể phân biệt nó với những thứ khác. Còn nếu ta phân biệt nó chỉ bởi các thuộc tính mô tả, khi đó nó là Value Object. Events đang là thuộc tính của Aggregate, liệu có thể là Value Objects được không?
Hãy để ý đến khía cạnh rộng hơn, với cách coi Events là thuộc tính thì trong suốt vòng đời của Aggregate, sẽ có thể có nhiều Events được sinh ra. Chúng cần được phân biệt với nhau. Vậy dựa trên thông tin gì để phân biệt? Xem xét hai Events DeliveryAddressChanged với cùng các thông tin sau:
- Cùng địa chỉ mới.
- Cùng OrderId.
- Cùng thời điểm xảy ra.

Vậy ta có thể yên tâm rằng hai Events đó cùng biểu diễn một sự kiện duy nhất chưa? Đứng trên quan điểm nghiệp vụ, tôi thấy rằng có thể. Vậy tôi sẽ coi Domain Events trong trường hợp này là những Value Objects, và do đó, không cần sinh ID cho chúng. Nếu có một tập Events trong tay (bên trong một Aggregate) tôi chỉ cần sắp xếp chúng theo thời gian xảy ra là có được lịch sử thay đổi của chúng rồi.
Tuy nhiên, trong một hệ thống mà một Aggregate có thể chỉnh sửa CỰC NHANH, một đơn vị thời gian (ở đây là milisecond) có thể thay đổi cùng 1 Aggregate nhiều lần khiến cho thời gian không còn đủ tin cậy để phân biệt nữa. Ta có thể cần tạo ID cho những Events đó. Trường hợp này có lẽ khá khó gặp trong thực tế.
Từ đây, nếu coi thời gian (occurredAt) là ID của Events, chúng có thể được coi là Entities. Vậy ta có lẽ không cần quá bận tâm Domain Events là gì, hãy coi chúng là... Domain Events mà thôi.
Có thể với những ai làm Transactional Outbox thấy rằng Events cần có IDs. Đó là khi ta lưu chúng vào bảng Outbox để chuẩn bị publish ra bên ngoài. Lúc này, ta gọi chúng là Messages: có ID, Type, Thời điểm xảy ra, và Payload chính là Domain Events.

Tới câu hỏi tiếp theo. Để trả lời câu hỏi này, cần xem xét yêu cầu nghiệp vụ. Nếu nghiệp vụ yêu cầu ghi lại toàn bộ sự kiện diễn ra trong vòng đời, khi đó, ta cần lưu lại tất cả các Events đó. Còn nếu không, không bắt buộc. Nhưng bạn sẽ thấy kể cả không bắt buộc thì ít nhất, chúng ta phải lưu nó lại như cơ chế Transactional Outbox để tính nhất quán giữa Aggregate và Domain Events Publishing được đảm bảo.
Trường hợp cần lưu lại toàn bộ.
Order Aggregate như hình 4 có vẻ ổn nếu một Order chỉ có vài lần cập nhật trong vòng đời của nó. Giả sử, phần lớn Order có rất nhiều lần thay đổi trạng thái - do đó là rất nhiều Events tương ứng bên trong, thì Aggregate này trở nên lớn, chiếm nhiều chi phí xử lý: Không gian lưu trữ tại database, memory, chi phí network, chi phí xử lý, ... Hãy tưởng tượng, khi số lượng người dùng ứng dụng đồng thời lớn thì chi phí này sẽ tăng lên gấp bội. Vậy hãy để ý đến khía cạnh này, không chỉ ở việc có nên lưu Events trong Aggregate hay không mà còn cả ở khía cạnh khi thiết kế Aggregate.
Giờ hãy để cập tới khía cảnh khai thác Events vừa sinh ra. Hãy xem tầng Application:
public class OrderManagementUseCase {
...
public void updateDeliveryAddress(OrderId orderId, Address newAddress) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.updateAddress(newAddress);
List filteredEvents = filterEvents(order.events());
eventStore.saveAll(filteredEvents);
orderRepository.save(order);
}
...
}
Sau khi order.updateAddress(...) được gọi, client sử dụng order.events() để lấy về Event xử lý. Tới đây, ta cần một cờ đánh dấu xem Event nào là Event mới phát sinh của use case này, nếu không sẽ có những Events bị publish lại. Với cách làm này, ta thấy rằng Events bị lưu duplicate ở 2 nơi: Outbox table và Aggregate. Giải quyết vấn đề này như thế nào? Có 2 cách.
Cách 1: Publish luôn Event tại Application. Event lưu cùng Aggregate không cần publish nữa.
Như vậy, code trên trở thành:
public class OrderManagementUseCase {
...
public void updateDeliveryAddress(OrderId orderId, Address newAddress) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.updateAddress(newAddress);
List filteredEvents = filterEvents(order.events());
eventPublisher.publish(filteredEvents);
orderRepository.save(order);
}
...
}
Cách này áp dụng cho những trường hợp không cần tính nhất quán dữ liệu cao: Aggregate và Event công bố ra ngoài không nhất thiết phải nhất quán với nhau. Việc lưu Events dưới database cũng thuận tiện hơn, ta có thể thoải mái lựa chọn cách lưu Events dưới dạng thuộc tính của Order (như trong Document Database) hoặc bảng tách biệt như Outbox table. Ngược lại, ta cần sử dụng Transactional Outbox như 2 cách tiếp theo.
Cách 2: Code Application chỉ coi Order Aggregate là điểm cần thao tác duy nhất (Application "không biết" hoặc "không quan tâm" tới sự tồn tại của Events. Việc xử lý ủy quyền sang Adapter):
public class OrderManagementUseCase {
...
public void updateDeliveryAddress(OrderId orderId, Address newAddress) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.updateAddress(newAddress);
orderRepository.save(order);
}
...
}
Đoạn code này cho thấy Application không cần quan tâm tới Events là gì. Việc bóc tách và publish sẽ do một tiến trình riêng phụ trách. Để làm điều đó, tầng Adapter khi thực hiện lưu Order Aggregate xuống database, cần tách Events ra riêng để lưu vào bảng riêng giống như Outbox table.

Với cách làm này, kết quả là giống với Transactional Outbox khi dữ liệu Order và Domain Events được lưu vào những bảng khác nhau trong cùng một database đảm bảo transactional. Tuy nhiên, DDD nhấn mạnh tính rõ ràng trong nghiệp vụ. Kể cả khi Application không cần filter by type để save toàn bộ Events xuống thì việc sinh ra Events này là trong suốt với Application. Vấn đề ở chỗ tầng Adapter lại biết sự có mặt của nó để mà bóc ra và xử lý! Hãy tránh điều này. Lớp ngoài sử dụng lớp trong thì lớp ngoài nên biết ít hơn lớp trong, đảm bảo tính trừu tượng trong thiết kế phần mềm. Ở đây, Application lại biết ít hơn lớp Adapter - là lớp bên ngoài so với Application.
Nếu sử dụng Document database như MongoDB, thông thường ta sẽ lưu một Aggregate trong một Document. Lúc này Events là thuộc tính của Aggregate, ta sẽ thấy khá khó khăn để lấy Events mới sinh ra để publish. Hãy chú ý vấn đề này khi làm việc với những database như vậy.
Coi Events là độc lập với Aggregate
Ngược lại với cách coi Events là thuộc tính của Aggregate, ta coi chúng độc lập tuy vẫn có mối quan hệ nhân quả. Điều này giúp ta giải quyết vấn đề Aggregate quá to hay logic xử lý bị rò rỉ từ Application ra Adapter.
Về bản chất, Order Aggregate vẫn như vậy: Vẫn chứa thuộc tính là một List nhưng ý nghĩa khác hẳn: Order chỉ lưu những Events vừa mới phát sinh trong luồng xử lý này, nó chỉ đóng vai trò là một Wrapper thuần túy. Vậy thì khi kết thúc Domain, Application cần lấy Events mới phát sinh ra và xử lý. Trách nhiệm của Domain chỉ là ghi nhận trạng thái thay đổi và sinh Events (Từ đầu tới giờ ta luôn làm vậy).
public class OrderManagementUseCase {
...
public void updateDeliveryAddress(OrderId orderId, Address newAddress) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new IllegalArgumentException("Order not found");
}
order.updateAddress(newAddress);
List filteredEvents = filterEvents(order.events());
eventStore.saveAll(filteredEvents);
orderRepository.save(order);
}
...
}
Ta thấy khá giống Cách 1 bên trên, chỉ khác là ta lưu Events độc lập với Order thôi. Đây là cơ chế thực hiện Transactional Outbox quen thuộc. Cách này đã mang tới cho Application sự tường minh: Gọi Aggregate thực thi hành vi nghiệp vụ, lấy Events phát sinh trong đó, lưu Events và Order vào hai nơi khác nhau.
(Xem tiếp phần 3.)