System Design Cases
Notification System
Multi-channel notification system (push, email, SMS, in-app) handling 100M DAU with 5.8K rps avg / 50K rps peak. Producers publish events to Kafka, orchestrator loads user preferences, dedups via Redis, renders templates, then dispatches to per-channel queues (q-push, q-email, q-sms). Channel workers (push/email/sms) post to APNs/FCM/SES/Twilio with retry-backoff and DLQ. Provider webhooks update ClickHouse for delivery status analytics.
Масштабируемая система уведомлений
Система принимает команды и доменные события, применяет пользовательские настройки, фиксирует durable intent и только затем публикует задания в каналы push, email и SMS. Главная гарантия — не «ровно одна доставка», а отсутствие потери принятого intent и идемпотентная обработка повторов.
Нагрузка
Проектное допущение: 500 млн уведомлений в сутки. Средняя частота равна 500 000 000 / 86 400 ≈ 5 787 сообщений/с. Пик 50 тыс./с — отдельное допущение примерно 8,6× среднего; его нужно подтвердить реальными часовыми и минутными профилями. Каналы планируются отдельно: у SMS есть стоимость и региональные лимиты, у email — reputation и bounce policy, у push — provider/device ограничения.
Почему intent и outbox обязательны
Если сначала записать SET NX в Redis, затем упасть до публикации, повтор будет ошибочно отброшен, а уведомление потеряется. Поэтому Orchestrator одной транзакцией:
- создаёт Notification Intent с уникальным tenant_id + dedupe_key;
- сохраняет выбранные каналы, template version, TTL и policy version;
- добавляет outbox rows для каналов.
Outbox Relay читает непубликованные строки и публикует их at-least-once. Повторная публикация безопасна, потому что worker дедуплицирует notification_id + channel + attempt. Redis допустим как быстрый hint, но не как источник истины.
Очереди, приоритет и повторы
Kafka упорядочивает записи только в пределах partition и не даёт универсальной отложенной очереди или строгого приоритета «partition 0 раньше остальных». Поэтому:
- critical и standard SMS имеют отдельные lanes и зарезервированную worker/provider capacity;
- retryable email не удерживает consumer во сне: задание попадает в delay tiers, а Retry Scheduler возвращает его в ready topic;
- terminal ошибки сразу фиксируются;
- DLQ логически отдельна для каждого канала, даже если на схеме показана одним узлом.
DLQ replay выполняется только после исправления причины, с прежним notification_id и контролируемым новым attempt. Слепой replay может повторно списать деньги или создать шторм.
Семантика provider status
Ответ APNs на HTTP/2 POST сообщает, принят или отклонён конкретный запрос. Для FCM возвращённый message ID также означает acceptance, а не доказанную доставку на устройство. Поэтому push worker пишет accepted_by_provider или rejected. Device delivery может оцениваться отдельной opt-in телеметрией FCM/клиента, но это другой, часто задержанный сигнал.
Amazon SES публикует delivery, bounce, complaint и другие события. Twilio отправляет status callbacks; они могут прийти с разной задержкой и не гарантированно по порядку. Status Service проверяет подпись, дедуплицирует provider + event_id и применяет только допустимые монотонные переходы. Неизвестное поле callback не должно ломать endpoint.
Политики пользователя
Preferences Store хранит opt-in/opt-out, locale, quiet hours и разрешённые каналы. Критические исключения должны быть явно определены продуктом и правом, а не обходить настройки по умолчанию. Template Service экранирует данные и сохраняет version, чтобы повтор воспроизводил тот же контент.
Сценарии
Durable push intent, outbox, provider acceptance и отдельная best-effort доставка на устройство.
Fan-out из одного доменного события в независимые каналы.
Retryable throttling через delay tiers, ограниченный retry budget и per-channel DLQ.
Повтор команды возвращает исходный notification_id благодаря уникальности в базе.
Отдельная critical SMS lane с зарезервированной пропускной способностью.
Failure semantics
- Event Bus и channel topics дают at-least-once; consumer commit выполняется после устойчивого результата.
- Если Intent DB недоступна, API не подтверждает приём.
- Если provider timeout оставляет неизвестный результат, worker сверяет provider id/idempotency key до нового send.
- Backpressure применяется per tenant и per provider; bulk traffic не может занять critical reserve.
- Метрики: outbox age, consumer lag, queue age по приоритету, acceptance rate, callback lag, retry/DLQ rate и stale token rate.
Связанные материалы
[CONCEPT]queues [CONCEPT]idempotency [CONCEPT]rate-limiting-algorithms [CONCEPT]observability-pillars [CONCEPT]consistency-models [CASE]rate-limiterПервичные источники
- APNs response handling: https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns
- FCM delivery reporting: https://firebase.google.com/docs/cloud-messaging/understand-delivery
- FCM TTL and collapsible messages: https://firebase.google.com/docs/cloud-messaging/customize-messages/setting-message-lifespan
- Debezium Outbox Event Router: https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html
- Apache Kafka design: https://kafka.apache.org/41/design/design/
- Amazon SES event contents: https://docs.aws.amazon.com/ses/latest/dg/event-publishing-retrieving-sns-contents.html
- Twilio outbound status callbacks: https://www.twilio.com/docs/messaging/guides/track-outbound-message-status