Platform — Khái niệm cốt lõi
Bốn khái niệm này là chìa khóa hiểu toàn bộ Platform. Nắm được chúng, các trang sau sẽ rất nhẹ.
EntityEvent — "có gì đó vừa đổi"
EntityEvent là một thông điệp nhỏ báo rằng một entity vừa thay đổi. Ví dụ: "Account #123 vừa được tạo", "Employee #45 vừa được sửa".
Mỗi event mang theo:
- EntityType — loại entity (Account, Attendee, Invoice, Employee…), mỗi loại một id số.
- EventType / ActionType — hành động gì (
Create_Account,Update_Employee…), cũng id số. - Định danh entity — id/guid để biết chính xác entity nào.
- (Tùy chọn) Payload / snapshot — ảnh chụp dữ liệu để xử lý hệ quả.
Service chỉ cần phát event vào hàng đợi entity-event-queue; nó không tự gửi email hay ghi history. Một worker sẽ đọc event và lo phần hệ quả. Đây là tinh thần event-driven: tách "việc xảy ra" khỏi "hệ quả của việc đó".
Side effect & Post-processor — các hệ quả độc lập
Từ một event có thể sinh nhiều hệ quả (side effect) độc lập:
Mỗi post-processor lo một concern (notification, history…), chạy độc lập: một cái lỗi không làm hỏng cái kia. Mỗi post-processor duyệt qua một mapping registry — tập các mapping nhỏ, mỗi mapping trả lời: "event này có sinh ra hệ quả của tôi không, và nếu có thì trông thế nào?". Mapping không tự lưu xuống DB; post-processor lo phần lưu + chống trùng đồng nhất.
Hybrid — hai cách tạo thông báo
Đây là quyết định kiến trúc quan trọng nhất. Có hai đường để tạo một NotificationJob:
| Đường | Ai tạo job | Dùng khi |
|---|---|---|
| Direct (action-derived) | Service tự tạo qua NotificationJobFactory | Cần context tính tại thời điểm gọi — diff trước/sau, comment của request, danh sách người nhận động |
| Event-driven (event-derived) | Service chỉ phát event; worker tạo job qua mapping | Thông báo là phản ứng thuần với một state đã đổi; người nhận/nội dung suy được từ entity + settings |
Cả hai đều đi qua cùng một NotificationJobFactory và cùng một pipeline gửi. Khác nhau chỉ ở ai bấm nút tạo job.
Vì sao không ép tất cả về một phía?
- Không ép hết sang event-driven vì:
- Mất context call-time: worker chỉ thấy state sau khi commit, không dựng lại được diff trước/sau hay input của request.
- Over-firing ⚠️: nhiều event hiện tại là CRUD chung (
Create_Account…) được bắn từ rất nhiều nơi cho mục đích audit. Nếu map thẳng chúng → notification, sẽ gửi rộng hơn nhiều so với hiện tại → spam người dùng.
- Không giữ hết direct vì: lặp contract ở ~49 chỗ (dễ sai), không idempotent (mỗi nơi tự
Guid.NewGuid()), service bị coupling với chi tiết notification.
➡️ Hybrid lấy ưu điểm cả hai: event-derived hưởng decoupling + idempotency; action-derived giữ được context. Cách an toàn để "opt" một event CRUD chung vào notification là gắn một hint (NotificationEventTypeId) tại đúng call-site có guard — xem Case thường gặp.
Idempotency — chống trùng bằng khóa tất định
Hàng đợi Azure là at-least-once: cùng một event có thể được giao (và xử lý) nhiều lần. Nếu mỗi lần xử lý lại tạo một dòng mới với khóa ngẫu nhiên (Guid.NewGuid()), ta sẽ có bản ghi trùng — email gửi 2 lần, history có 2 dòng.
Giải pháp: deterministic key — khóa suy ra từ danh tính của sự kiện, không random.
- Notification event-derived: dùng
NotificationEventKey.ForEntityEvent(...)→ cùng event cho ra cùng key ⇒ retry không nhân đôi. - History: dùng
HistoryEventKey.ForEntityEvent(...)làmRowKey+ ghi insert-or-replace ⇒ retry ghi đè đúng một dòng, hai event thật sự khác nhau thì khác key (giữ lịch sử). - Action-derived (direct): cố ý giữ
Guid.NewGuid()(at-least-once), vì các flow này không có danh tính replay ổn định; người nhận/nội dung tính tại call-time.
Quy tắc vàng: một loại thông báo chỉ được sinh bởi đúng một đường (event-derived hoặc direct, không cả hai) — nếu không sẽ gửi 2 lần. Có một test guardrail dual-path để ép buộc điều này.
Tiếp theo: xem các khái niệm này ráp lại thành hệ thống ở Kiến trúc.