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.
EntityEvent payload và BlobRef
EntityEvent được serialize thành một message hoàn chỉnh trước khi kiểm tra giới hạn queue. Nếu message vượt giới hạn khoảng 45 KiB, payload không rỗng được lưu ở Blob và queue giữ reference để worker đọc lại. Event có payload/blob reference bắt buộc nhưng không đọc được sẽ fail-closed, không tự dựng một payload rỗng.
Ví dụ thực tế: Snapshot lịch sử lớn không bị cắt ngắn trong queue; worker đọc
BlobRef, khôi phục payload rồi mới chạy mapping history/notification.
SourceKey trong traceability
SourceKey là khóa theo nguồn nghiệp vụ, dùng để nối NotificationJob và OutgoingMessage với invoice, account hoặc entity tương ứng. Khóa này không thay thế deterministic key của idempotency; nó phục vụ tra lịch sử và điều phối message ở message grain.
Ví dụ thực tế: Hai người nhận của cùng invoice có hai dòng
OutgoingMessage, nhưng cùngSourceKeyđể support xem toàn bộ kết quả gửi của invoice đó.