🔗 Trận 3: Thiết kế Notification System – Làm sao để gửi hàng chục triệu thông báo mỗi ngày?
Mỗi ngày, điện thoại của chúng ta nhận hàng chục, thậm chí hàng trăm thông báo.
- Shopee báo Flash Sale.
- Facebook báo có người bình luận.
- Grab báo tài xế đã đến.
- Ngân hàng gửi mã OTP.
- Gmail báo có email mới.
Đằng sau những dòng thông báo xuất hiện chỉ trong vài giây ấy là cả một hệ thống phải hoạt động liên tục 24/7.
Nếu hôm nay bạn bước vào một buổi System Design Interview, người phỏng vấn đưa ra đề bài:
"Hãy thiết kế một Notification System có thể phục vụ hàng chục triệu người dùng."
Bạn sẽ bắt đầu từ đâu?
Đây chính là lúc phương pháp của Alex Xu phát huy tác dụng.
Đừng vội nghĩ đến Kafka, Redis hay Kubernetes.
Điều đầu tiên cần làm là hiểu đúng bài toán.
Bước 1. Hiểu yêu cầu (Requirement Clarification)
Một Senior Engineer kinh nghiệm sẽ dành vài phút đầu chỉ để... đặt câu hỏi.
Bởi vì một hệ thống tốt chỉ được xây dựng khi chúng ta hiểu rõ yêu cầu.
Những câu hỏi đầu tiên có thể là:
Hệ thống cần hỗ trợ những loại thông báo nào?
Người phỏng vấn trả lời:
- Mobile Push trên iOS
- Mobile Push trên Android
- SMS
Điều này đồng nghĩa với việc hệ thống sẽ phải làm việc với nhiều nhà cung cấp khác nhau thay vì chỉ một giao thức duy nhất.
Có yêu cầu Realtime không?
"Càng nhanh càng tốt." Đây là câu trả lời khá phổ biến.
Nhưng người phỏng vấn bổ sung thêm:
Nếu chậm vài giây khi hệ thống quá tải thì vẫn chấp nhận được.
Chi tiết nhỏ này cực kỳ quan trọng.
Nó mở ra khả năng sử dụng xử lý bất đồng bộ (Asynchronous Processing) thay vì bắt buộc phải gửi ngay lập tức.
Ai là người tạo Notification?
Không phải ứng dụng trên điện thoại.
Thông báo luôn được khởi tạo từ phía Server.
Ví dụ:
- Auth Service gửi OTP.
- Order Service thông báo giao hàng.
- Billing Service gửi hóa đơn.
- Marketing Service gửi khuyến mãi.
Điều này có nghĩa Notification System sẽ đóng vai trò là một dịch vụ dùng chung (Shared Infrastructure) cho toàn bộ hệ thống.
Người dùng có thể tắt Notification không?
Có.
Người dùng hoàn toàn có thể:
- Tắt Email
- Vẫn nhận SMS
- Chỉ nhận Push Notification
Đây sẽ là một yêu cầu cần được xử lý trước khi gửi thông báo.
Bước 2. Ước lượng quy mô (Back-of-the-envelope Estimation)
Alex Xu luôn nhấn mạnh rằng:
Đừng thiết kế khi chưa biết hệ thống lớn đến đâu.
Giả sử hệ thống cần xử lý mỗi ngày:
- 10 triệu Push Notification
- 1 triệu SMS
- 5 triệu Email
Tổng cộng: 16 triệu Notification/ngày
Chia trung bình:
16,000,000 / 86,400 giây ≈ 185 request/giây
185 request/giây nghe có vẻ không quá lớn.
Nhưng đây chỉ là trung bình.
Trong thực tế, Notification thường xuất hiện theo từng đợt.
Ví dụ:
- 9 giờ sáng gửi email marketing.
- 20 giờ tối Flash Sale bắt đầu.
- Black Friday.
- 11/11.
- 12/12.
Khi đó lưu lượng có thể tăng gấp hàng chục lần.
Vì vậy, thay vì thiết kế cho 185 RPS, chúng ta cần một hệ thống có thể mở rộng linh hoạt khi xảy ra spike.
Bước 3. Thiết kế kiến trúc tổng quan
Ý tưởng đầu tiên rất tự nhiên. Mỗi Service chỉ cần gọi Notification Server.
Notification Server sẽ gọi trực tiếp đến các nhà cung cấp.

Thoạt nhìn có vẻ hoàn toàn hợp lý. Nhưng hãy thử tưởng tượng một tình huống.
- Twilio đang gặp sự cố.
- API phản hồi mất 15 giây.
Notification Server sẽ phải đứng chờ.
Trong khoảng thời gian đó:
- Thread bị chiếm.
- Request mới tiếp tục đổ vào.
- Thread Pool đầy.
- Toàn bộ hệ thống bắt đầu chậm theo.
Chỉ vì một nhà cung cấp bên thứ ba.
Đây chính là dấu hiệu cho thấy kiến trúc đang coupling quá chặt.
Chúng ta cần tách việc nhận yêu cầu và gửi Notification thành hai công việc độc lập.
Bước 4. Chuyển sang xử lý bất đồng bộ
Giải pháp quen thuộc trong System Design là sử dụng Message Queue.
Notification Server chỉ làm đúng một nhiệm vụ:
- Validate request.
- Tạo payload.
- Đẩy vào Queue.

Worker sẽ liên tục lấy dữ liệu từ Queue để gửi.
Lúc này hệ thống có nhiều ưu điểm.
- Nếu Twilio chậm? Worker sẽ chờ.
- Notification Server vẫn tiếp tục nhận request.
Nếu cần xử lý gấp 10 lần lưu lượng?
- Chỉ cần tăng số lượng Worker.
- Không cần sửa Notification Server.
Đây là một ví dụ điển hình của Loose Coupling.
Bước 5. Nhưng Queue có đủ an toàn không?
Giả sử Worker vừa lấy được một OTP từ Queue. Nó chuẩn bị gửi. Đúng lúc đó máy chủ mất điện. Notification biến mất. Người dùng không bao giờ nhận được OTP.
Đây là lỗi không thể chấp nhận.
Alex Xu đưa ra một nguyên tắc rất đáng nhớ:
Queue không phải là Source of Truth.
Ngay khi nhận request.
Notification Server sẽ ghi Notification xuống Database trước.

Database lúc này trở thành nơi lưu trữ chính.
Nếu Worker crash. Nếu Queue lỗi. Nếu cả cụm server khởi động lại.
Chúng ta vẫn có thể đọc Notification Log để gửi lại.
Đây chính là nền tảng của cơ chế Retry.
Bước 6. Retry lại tạo ra một bài toán khác
Retry giúp tránh mất dữ liệu. Nhưng lại sinh ra một vấn đề mới.
Giả sử Worker gửi thành công. Twilio đã nhận được. Ngay sau đó mạng bị đứt. Worker tưởng rằng gửi thất bại. Nó Retry.
Kết quả là người dùng nhận hai SMS giống hệt nhau.
Đây là hiện tượng rất phổ biến trong các hệ thống phân tán.
Giải pháp là Idempotency.

Mỗi Notification được gán một event_id.
Trước khi gửi. Worker kiểm tra Redis.
event_id đã xử lý chưa?
-
Nếu rồi. Bỏ qua.
-
Nếu chưa. Tiếp tục gửi và đánh dấu đã hoàn thành.
Nhờ đó dù Retry bao nhiêu lần. Người dùng cũng chỉ nhận đúng một Notification.
Bước 7. Những thành phần nhỏ nhưng cực kỳ quan trọng
Đến đây hệ thống đã hoạt động. Nhưng một Senior Engineer sẽ không dừng lại. Họ sẽ tiếp tục nghĩ đến những tình huống trong thực tế.
User Settings
Nếu người dùng đã tắt Email. Đừng đưa Email vào Queue. Đó chỉ là lãng phí tài nguyên.
Notification Templates
Thay vì để từng Service tự viết HTML Email.
Ta xây dựng một kho Template.
Ví dụ:
Xin chào {{user_name}}
Đơn hàng {{order_id}}
đã được giao thành công.
Service chỉ cần truyền dữ liệu.
Notification Server sẽ render nội dung cuối cùng.
Rate Limiting
Không ai thích nhận 50 Push Notification mỗi ngày.
Hệ thống nên giới hạn:
- tối đa 5 Push/giờ
- hoặc 50 Push/ngày
Điều này vừa bảo vệ trải nghiệm người dùng vừa giảm nguy cơ bị gỡ ứng dụng.
Tracking & Analytics
Đối với đội vận hành.
"Gửi thành công" chưa đủ.
Họ muốn biết Notification đang ở đâu.
Created
↓
Pending
↓
Sent
↓
Delivered
↓
Clicked
Mỗi trạng thái đều được ghi nhận.
Nhờ đó doanh nghiệp có thể thống kê:
- Delivery Rate
- Open Rate
- Click Through Rate
- Thời gian giao trung bình
Trade-off và khả năng mở rộng
Đây là phần mà Alex Xu luôn dành vài phút cuối buổi phỏng vấn để thảo luận.
Một thiết kế tốt không phải là thiết kế không có nhược điểm.
Mà là thiết kế hiểu rõ những gì mình đang đánh đổi.
Đồng bộ hay bất đồng bộ?
Gọi API trực tiếp sẽ đơn giản hơn. Độ trễ thấp hơn.
Nhưng chỉ cần một Third-party Provider gặp sự cố là toàn bộ hệ thống bị ảnh hưởng.
Ngược lại, Message Queue giúp hệ thống chịu tải tốt hơn, nhưng đổi lại Notification có thể đến chậm vài giây.
Đó là một trade-off hoàn toàn hợp lý.
Một Queue hay nhiều Queue?
Nếu tất cả Notification dùng chung một Queue. Email Marketing khổng lồ có thể làm chậm cả OTP.
Giải pháp tốt hơn là tách Queue theo từng kênh.
- Push Queue
- SMS Queue
- Email Queue
Thậm chí có thể tách riêng Queue cho các Notification ưu tiên cao như OTP.
Làm sao để mở rộng?
Khi lưu lượng tăng từ 10 triệu lên 100 triệu Notification mỗi ngày.
Kiến trúc gần như không cần thay đổi.
Chúng ta chỉ cần:
- tăng Notification Worker
- mở rộng Message Queue
- scale Notification Server theo chiều ngang
- thêm nhiều Provider để cân bằng tải
Đó chính là ưu điểm lớn nhất của kiến trúc bất đồng bộ.
Tổng kết
Điều mình thích nhất trong cách tiếp cận của Alex Xu là ông không cố gắng xây dựng một kiến trúc hoàn hảo ngay từ đầu.
Thay vào đó, hệ thống được cải tiến từng bước, và mỗi quyết định đều xuất phát từ một vấn đề thực tế:
- Gọi trực tiếp Third-party dễ gây nghẽn → thêm Message Queue.
- Queue có thể làm mất dữ liệu → thêm Notification Log và Retry.
- Retry gây gửi trùng → thêm Idempotency.
- Gửi quá nhiều làm phiền người dùng → thêm Rate Limiting.
- Người dùng muốn kiểm soát thông báo → thêm User Settings.
- Doanh nghiệp muốn đo lường hiệu quả → thêm Tracking & Analytics.
Đó cũng chính là tư duy mà các công ty lớn tìm kiếm trong một buổi System Design Interview: không phải người nhớ nhiều công nghệ nhất, mà là người biết đặt câu hỏi đúng, nhìn thấy vấn đề và từng bước tiến hóa kiến trúc để giải quyết chúng.
All rights reserved