Điểm mấu chốt: service nghiệp vụ không được biết đang gửi bằng kênh nào. Chúng chỉ phát một sự kiện có ý nghĩa nghiệp vụ; việc chọn kênh, dựng nội dung, giới hạn tần suất nằm ở hệ thống thông báo.
Luồng:
service -> event (order.delivered) -> notification-service
-> template render -> policy check -> channel worker (push/sms/email)
-> provider -> delivery status callbackData model chính:
notification_templates(key, channel, locale, subject, body)
user_preferences(user_id, channel, category, enabled, quiet_hours)
notifications(id, user_id, template_key, dedupe_key unique, status, attempts)Chọn kênh: ưu tiên theo chi phí và độ tin cậy — push (gần như miễn phí) → email → SMS cuối cùng vì đắt nhất; ở VN, OTP/SMS brandname là khoản chi đáng kể nên chỉ dùng cho việc bắt buộc. Có thể fallback theo bậc: push không được xác nhận trong 5 phút thì mới bắn SMS.
Chống spam:
- dedupe_key (vd order:{id}:delivered) có unique index → cùng một sự kiện phát lại không gửi hai lần.
- Giới hạn tần suất theo người: tối đa N thông báo/giờ mỗi nhóm; gom các thông báo cùng loại thành một bản tóm tắt.
- Tôn trọng quiet_hours và trạng thái tắt của người dùng — bỏ qua thì tỉ lệ gỡ app tăng.
Nhà cung cấp lỗi: đây là phần hay bị hỏi sâu. Retry với exponential backoff kèm jitter (retry đồng loạt sẽ tự tạo ra đợt tải thứ hai), đặt circuit breaker cho mỗi provider, và cấu hình sẵn provider dự phòng. Message quá hạn hoặc hỏng chuyển vào DLQ để xử lý riêng, không chặn hàng đợi chính.
Đánh đổi: đảm bảo at-least-once là lựa chọn thực tế (mất thông báo tệ hơn trùng), nên khử trùng lặp bằng dedupe_key ở phía nhận. Muốn exactly-once đầu-cuối qua nhà cung cấp bên ngoài là bất khả thi — hãy nói thẳng điều đó.