Email, SMS, push; retry, hàng đợi, template, hẹn giờEmail, SMS, push; retries, queues, templates, scheduling
API ghi yêu cầu và sự kiện trong cùng một transaction rồi trả về ngay. Worker riêng cho từng kênh đọc hàng đợi và gọi nhà cung cấp. Bật “Nhà cung cấp SMS lỗi” để xem retry, circuit breaker và DLQ.The API stores the request and its event in one transaction and answers immediately. A worker per channel reads the queue and calls the provider. Turn on “SMS provider fails” to see retries, the circuit breaker and the DLQ.
1Auth service cần gửi OTP cho user 42.The auth service needs to send an OTP to user 42.
Consumer nhận lại cùng message sau khi crash. Dùng idempotency_key có unique index; worker khoá dòng và bỏ qua nếu đã SENT.A consumer gets the same message again after a crash. Keep an idempotency_key with a unique index; the worker locks the row and skips it when already SENT.
Exponential backoff + jitter (1 s, 2 s, 4 s…, tối đa 5 lần), circuit breaker theo nhà cung cấp; quá số lần thì vào DLQ.Exponential backoff with jitter (1s, 2s, 4s…, up to 5 attempts), a circuit breaker per provider, then the DLQ.
Ghi notification và outbox trong cùng transaction; relay đẩy sang Kafka/RabbitMQ sau khi commit.Write the notification and the outbox row in one transaction; a relay publishes to Kafka/RabbitMQ after commit.
Tách hàng theo độ ưu tiên critical / default / bulk, mỗi hàng có worker và giới hạn tốc độ riêng.Split queues by priority (critical / default / bulk), each with its own workers and rate limit.
Strategy chọn kênh, Template Method cho render → gửi → ghi kết quả, Adapter bọc SES, Twilio, FCM/APNs.Strategy picks the channel, Template Method runs render → send → record, Adapter wraps SES, Twilio, FCM/APNs.
Job quét bảng lịch dùng SELECT … FOR UPDATE SKIP LOCKED để hai instance không lấy cùng một tin.The scheduler polls with SELECT … FOR UPDATE SKIP LOCKED so two instances never pick the same message.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
notification | id, user_id, channel, template_code, payload, status, priority, idempotency_key, scheduled_at | unique(idempotency_key); PENDING → SENDING → SENT · FAILED · DEADunique(idempotency_key); PENDING → SENDING → SENT · FAILED · DEAD |
notification_attempt | notification_id, attempt, provider, error, latency_ms, created_at | Lịch sử từng lần gửi để tra lỗiEvery attempt, for troubleshooting |
template | code, channel, locale, subject, body, version | Biến name, otp, link được thay khi renderVariables such as name, otp, link are filled in at render time |
user_preference | user_id, category, channel, enabled | Tắt marketing nhưng vẫn nhận OTPMarketing off, OTPs still on |
outbox | id, topic, key, payload, published_at | Relay đẩy sang message brokerPublished to the broker by a relay |
Worker nhận message, bỏ qua nếu đã gửi, lỗi thì để framework thử lại; quá 5 lần thì chuyển vào DLQ.The worker receives a message, skips it if already sent, lets the framework retry on errors and moves it to the DLQ after 5 attempts.
@Component
@RequiredArgsConstructor
public class EmailWorker {
private final NotificationRepository repo;
private final EmailProvider provider;
private final DeadLetterPublisher dlq;
@KafkaListener(topics = "notify.email")
@Transactional
public void handle(NotifyEvent e) {
Notification n = repo.lockById(e.id()); // SELECT … FOR UPDATE
if (n.isSent()) return; // duplicate delivery
try {
provider.send(render(n));
n.markSent();
} catch (ProviderException ex) {
n.recordFailure(ex); // attempt++
if (n.attempts() >= 5) dlq.send(e);
else throw ex; // retried with backoff
}
}
}type EmailWorker struct {
tx TxRunner
provider EmailProvider
dlq Publisher
}
func (w *EmailWorker) Handle(ctx context.Context, e NotifyEvent) error {
return w.tx.Do(ctx, func(tx Tx) error {
n, err := tx.LockNotification(ctx, e.ID) // FOR UPDATE
if err != nil || n.Sent() {
return err // nil when already sent
}
if err := w.provider.Send(ctx, render(n)); err != nil {
n.RecordFailure(err)
if n.Attempts >= 5 {
return w.dlq.Publish(ctx, e)
}
return fmt.Errorf("retry: %w", err)
}
return tx.MarkSent(ctx, n.ID)
})
}| KafkaKafka | RabbitMQRabbitMQ | |
|---|---|---|
| Hợp vớiGood for | Lưu lượng lớn, phát lại sự kiện, nhiều consumer groupHigh volume, replay, many consumer groups | Hàng đợi tác vụ, định tuyến linh hoạt, ưu tiênTask queues, flexible routing, priorities |
| Retry, DLQRetry, DLQ | Tự làm bằng topic retry + DLQBuild it with retry topics and a DLQ topic | Có sẵn dead-letter exchange, TTLDead-letter exchanges and TTL built in |
| Thứ tựOrdering | Theo partition (key = user_id)Per partition (key = user_id) | Theo queue, mất khi nhiều consumerPer queue, lost with many consumers |
| Chọn khiPick it when | Đã có Kafka, cần audit hoặc phát lạiKafka is already there, you need audit or replay | Team nhỏ, cần ưu tiên OTP đơn giảnA small team needs simple OTP priority |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Message được giao lại và worker gửi lần hai. Chặn bằng trạng thái SENT có khoá dòng, hoặc idempotency key phía nhà cung cấp nếu có. “Exactly-once” trong thực tế là at-least-once cộng xử lý idempotent.The message is redelivered and the worker sends again. Prevent it with a SENT status under a row lock, or a provider-side idempotency key when available. In practice “exactly once” is at-least-once plus idempotent handling.
Tách hàng đợi theo độ ưu tiên, mỗi hàng có worker và giới hạn tốc độ riêng; campaign đi hàng bulk, OTP đi hàng critical.Separate queues by priority, each with its own workers and rate limit; campaigns use bulk, OTPs use critical.
Dồn tải lên nhà cung cấp đang lỗi (retry storm). Dùng exponential backoff có jitter và circuit breaker.It piles load on a provider that is already failing (a retry storm). Use exponential backoff with jitter and a circuit breaker.
DB và Kafka không có cam kết chung: transaction có thể rollback sau khi tin đã đi, hoặc commit xong rồi service chết trước khi gửi. Outbox gom cả hai vào một lần commit DB.The DB and Kafka share no commit: the transaction can roll back after the message left, or commit and then the service dies before sending. The outbox puts both into one DB commit.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.