一個事件、三個出口:通知系統的解耦練習
平台有三個長得很像的模組:站內通知(DB + 讀取 API)、SSE 即時推播、outbound webhook(對外部訂閱者投遞)。最初的直覺是把它們做成一個「通知服務」,provider 呼叫時指定要走哪些管道——這個直覺是錯的。
正確的形狀:producer 只派發領域事件,三個出口各自是 listener。
code
NotificationService.send()
→ 寫 DB → 派發 notification.created
├─ SSE listener(白名單內的事件推給連線者)
└─ webhook bridge(比對訂閱 pattern → 丟 jobs queue 投遞)
寄信也是第四個潛在出口——信件事件規則訂閱 pattern 命中就寄。要新增/移除管道,改的永遠是 listener 端,producer 一個字不動。
兩個細節值得記
SSE 用白名單,webhook 用全收。SSE 是廣播——每個連線者都收得到,所以哪些事件能上廣播要白名單審核(PII 風險);webhook 是訂閱制,訂閱者自己聲明 pattern,橋接器全收再過濾。同樣是「事件出口」,信任模型完全不同。
Listener 的失敗不回滾主操作。in-process 事件是 best-effort——通知寄失敗不該讓業務寫入跟著死。需要 at-least-once 保證的出口(webhook 投遞)不直接做事,而是把工作丟進 jobs queue 讓重試機制接手。「事件」與「保證」是兩層,混在一起就會出現「為了通知的可靠性把業務交易搞複雜」的怪設計。
還有一個防呆彩蛋:webhook 橋接器會跳過名字以 webhook. 開頭的事件——不然投遞成功本身的事件又會觸發投遞,遞迴到天荒地老。
讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。