Domain Events - Giao Tiếp Giữa Các Bounded Context¶
1. Domain Events Là Gì?¶
Định Nghĩa Đơn Giản¶
Domain Event là một thông báo về việc gì đó quan trọng đã xảy ra trong domain.
Ví dụ thực tế:
- Khi khách hàng đặt hàng → OrderPlacedEvent
- Khi thanh toán thành công → PaymentCompletedEvent
- Khi hàng được giao → OrderDeliveredEvent
Tại Sao Cần Domain Events?¶
** Không có Events:**
OrderService.createOrder() {
// 1. Tạo đơn hàng
order = new Order(...)
orderRepo.save(order)
// 2. Trừ kho
inventoryService.reserveStock(order.items)
// 3. Gửi email
emailService.sendOrderConfirmation(order)
// 4. Tính point thưởng
loyaltyService.addPoints(order.customerId)
// 5. Ghi log
auditService.logOrderCreated(order)
}
Vấn đề: OrderService biết quá nhiều thứ! Nếu thêm tính năng mới phải sửa OrderService.
** Có Events:**
OrderService.createOrder() {
// 1. Tạo đơn hàng
order = new Order(...)
// 2. Phát sự kiện
order.addEvent(new OrderPlacedEvent(order))
// 3. Lưu
orderRepo.save(order) // Events tự động được publish
}
// Các service khác tự động lắng nghe
InventoryService.handle(OrderPlacedEvent event) {...}
EmailService.handle(OrderPlacedEvent event) {...}
LoyaltyService.handle(OrderPlacedEvent event) {...}
Lợi ích: OrderService chỉ lo tạo đơn hàng. Các logic khác tự động kích hoạt.
2. Workflow Domain Events¶
sequenceDiagram
participant User
participant OrderController
participant OrderService
participant Order
participant EventBus
participant InventoryService
participant EmailService
participant LoyaltyService
User->>OrderController: POST /orders
OrderController->>OrderService: createOrder()
OrderService->>Order: new Order()
Order->>Order: addEvent(OrderPlacedEvent)
OrderService->>OrderService: orderRepo.save(order)
OrderService->>EventBus: publish(OrderPlacedEvent)
EventBus->>InventoryService: handle(OrderPlacedEvent)
EventBus->>EmailService: handle(OrderPlacedEvent)
EventBus->>LoyaltyService: handle(OrderPlacedEvent)
InventoryService-->>EventBus: StockReservedEvent
EmailService-->>EventBus: EmailSentEvent
LoyaltyService-->>EventBus: PointsAddedEvent
3. Ví Dụ Thực Tế: Hệ Thống E-commerce¶
Tình Huống: Khách Hàng Đặt Hàng¶
Events Được Tạo Ra:¶
flowchart TD
A["🛒 User đặt hàng"] --> B["📢 OrderPlacedEvent"]
B --> C[" InventoryService<br/>trừ kho"]
B --> D[" EmailService<br/>gửi email"]
B --> E[" LoyaltyService<br/>tính điểm"]
B --> F[" AnalyticsService<br/>tracking"]
C --> G[" StockReservedEvent"]
D --> H[" EmailSentEvent"]
E --> I[" PointsEarnedEvent"]
G --> J[" ShippingService<br/>chuẩn bị giao hàng"]
Chi Tiết Events:¶
1. OrderPlacedEvent
{
"eventId": "evt_001",
"eventType": "OrderPlaced",
"occurredOn": "2024-01-15T10:30:00Z",
"data": {
"orderId": "ORD_12345",
"customerId": "CUST_999",
"items": [
{"productId": "PROD_A", "quantity": 2, "price": 500000},
{"productId": "PROD_B", "quantity": 1, "price": 300000}
],
"totalAmount": 1300000,
"orderStatus": "CONFIRMED"
}
}
2. StockReservedEvent
{
"eventId": "evt_002",
"eventType": "StockReserved",
"occurredOn": "2024-01-15T10:30:05Z",
"data": {
"orderId": "ORD_12345",
"reservations": [
{"productId": "PROD_A", "quantityReserved": 2, "warehouseId": "WH_001"},
{"productId": "PROD_B", "quantityReserved": 1, "warehouseId": "WH_002"}
]
}
}
3. PaymentProcessedEvent
{
"eventId": "evt_003",
"eventType": "PaymentProcessed",
"occurredOn": "2024-01-15T10:32:00Z",
"data": {
"orderId": "ORD_12345",
"paymentId": "PAY_777",
"amount": 1300000,
"paymentMethod": "CREDIT_CARD",
"status": "SUCCESS"
}
}
Event Handlers:¶
InventoryService
handle(OrderPlacedEvent event) {
foreach item in event.items {
if (hasStock(item.productId, item.quantity)) {
reserveStock(item.productId, item.quantity)
} else {
publish(OutOfStockEvent(item.productId))
}
}
publish(StockReservedEvent(...))
}
EmailService
handle(OrderPlacedEvent event) {
customer = customerRepo.findById(event.customerId)
orderDetails = formatOrderDetails(event)
sendEmail(
to: customer.email,
subject: "Xác nhận đơn hàng #" + event.orderId,
body: orderDetails
)
publish(EmailSentEvent(...))
}
LoyaltyService
handle(OrderPlacedEvent event) {
points = calculatePoints(event.totalAmount)
addPointsToCustomer(event.customerId, points)
publish(PointsEarnedEvent(...))
}
4. Pattern: Event Sourcing vs Domain Events¶
Domain Events (Event-Driven)¶
Mục đích: Giao tiếp giữa các Bounded Context Lưu trữ: Event chỉ để thông báo, data chính vẫn ở database thường Ví dụ: OrderPlacedEvent → InventoryService trừ kho
flowchart LR
A[" Order DB<br/>(State)"] --> B["📢 OrderPlacedEvent<br/>(Notification)"]
B --> C[" Inventory Service"]
B --> D[" Email Service"]
Event Sourcing¶
Mục đích: Lưu trữ toàn bộ lịch sử thay đổi Lưu trữ: Events chính là source of truth Ví dụ: OrderCreated → OrderItemAdded → OrderConfirmed → OrderShipped
flowchart TD
A[" Event Store"] --> B["OrderCreatedEvent"]
A --> C["OrderItemAddedEvent"]
A --> D["OrderConfirmedEvent"]
A --> E["OrderShippedEvent"]
F[" Current State"] --> G["Rebuild từ Events"]
B --> G
C --> G
D --> G
E --> G
️ Lưu ý: Domain Events ≠ Event Sourcing. Có thể dùng riêng lẻ hoặc kết hợp.
5. Các Loại Events Thường Gặp¶
1. Entity Lifecycle Events¶
Khi Entity được tạo/sửa/xóa:
- CustomerRegisteredEvent
- ProductUpdatedEvent
- OrderCancelledEvent
2. Business Process Events¶
Khi quy trình kinh doanh xảy ra:
- PaymentProcessedEvent
- InventoryReplenishedEvent
- ShipmentDeliveredEvent
3. Integration Events¶
Khi cần đồng bộ với hệ thống ngoài:
- OrderSyncedToERPEvent
- CustomerExportedToCRMEvent
4. Time-based Events¶
Khi có sự kiện định kỳ:
- MonthlyReportGeneratedEvent
- SubscriptionExpiredEvent
6. Event Implementation Patterns¶
Pattern 1: In-Memory Events¶
// Trong cùng process
EventBus.publish(event)
EventBus.subscribe(handler)
Ưu điểm: Đơn giản, nhanh
Nhược điểm: Mất event nếu crash
Pattern 2: Message Queue¶
// Qua RabbitMQ, Kafka, AWS SQS
queue.send(event)
consumer.receive(event)
Ưu điểm: Reliable, scalable
Nhược điểm: Phức tạp, eventually consistent
Pattern 3: Database Events¶
// Lưu events vào DB, polling hoặc CDC
eventsTable.insert(event)
eventProcessor.poll(events)
Ưu điểm: Transactional, audit trail
Nhược điểm: Performance overhead
Pattern 4: Hybrid (Outbox Pattern)¶
// Lưu event cùng transaction, sau đó publish
transaction {
orderRepo.save(order)
eventsTable.insert(event)
}
eventPublisher.publishPendingEvents()
Ưu điểm: Best of both worlds
Nhược điểm: Implementation complexity
7. Best Practices¶
Naming Convention¶
- Past Tense:
OrderPlaced,PaymentProcessed(đã xảy ra) - Domain Language: Dùng thuật ngữ business hiểu được
- Specific:
CustomerRegisteredthay vìCustomerChanged
Event Design¶
- Immutable: Event không bao giờ thay đổi
- Self-contained: Chứa đủ thông tin cần thiết
- Versioned: Có version để tương thích backward
Handler Design¶
- Idempotent: Chạy nhiều lần cũng cùng kết quả
- Fast: Không block quá lâu
- Error Handling: Retry logic, dead letter queue
Testing¶
- Unit Test: Test từng handler riêng lẻ
- Integration Test: Test end-to-end flow
- Event Store: Test event được tạo đúng
8. Common Pitfalls¶
Lỗi 1: Event Coupling¶
// SAI: Event chứa quá nhiều detail
CustomerRegisteredEvent {
customer: Customer,
preferences: UserPreferences,
billingInfo: BillingInfo,
shippingInfo: ShippingInfo
}
// ĐÚNG: Event chỉ chứa essential info
CustomerRegisteredEvent {
customerId: string,
email: string,
registeredAt: Date
}
Lỗi 2: Synchronous Event Handling¶
// SAI: Handler chậm làm chậm toàn bộ process
OrderPlacedEvent → EmailHandler (5s) → timeout
// ĐÚNG: Async processing
OrderPlacedEvent → Queue → EmailHandler (background)
Lỗi 3: Event Versioning Issues¶
// SAI: Thay đổi event structure
OrderPlacedEvent v1: { orderId, amount }
OrderPlacedEvent v2: { orderId, items[] } // Break consumers
// ĐÚNG: Backward compatible
OrderPlacedEvent v1: { orderId, amount }
OrderPlacedEvent v2: { orderId, amount, items[] } // Keep old fields
9. Kết Luận¶
Domain Events giúp: - Loose Coupling: Các service không phụ thuộc trực tiếp - Scalability: Dễ dàng thêm tính năng mới - Auditability: Tracking được mọi sự kiện - Integration: Giao tiếp giữa Bounded Context
Nguyên tắc vàng:
"Don't call us, we'll call you" - Events cho phép inversion of control, hệ thống linh hoạt hơn rất nhiều!