Notification System — Failure Scenarios¶
Themes: third-party providers are unreliable (the whole system is built to survive that), never let bulk starve transactional, never double-send or send after opt-out, and accept ingestion even when downstream is down.
A provider goes down (e.g. SES / Twilio outage)¶
- Impact: that channel can't deliver via the primary provider.
- Detect: spike in errors/timeouts from the provider adapter; delivery rate drops.
- Contain: circuit breaker trips → fail over to the secondary provider via the channel abstraction; messages keep flowing. If no secondary, messages stay queued (buffered) and drain when the provider recovers — delayed, not lost.
- Recover: breaker half-opens to test recovery; resume primary. Backlog drains at rate-limited pace.
A provider throttles us (429)¶
- Impact: sends rejected for exceeding quota.
- Detect: 429 rate.
- Contain: this is not a failure — slow the lane to the provider's rate limit and requeue; pace campaigns. Adding workers won't help (the ceiling is external). Optionally spread load across a second provider.
Bulk campaign starves transactional (priority inversion)¶
- Impact: OTPs delayed behind a 50M-user blast → users can't log in.
- Contain: separate queues/pools for transactional vs bulk per channel → the transactional lane is short and drains fast regardless of the bulk backlog. This is why lanes are physically separate, not just prioritized flags.
Duplicate sends¶
- Impact: user gets the same notification twice (annoying; for OTP, confusing).
- Cause: at-least-once queue redelivery, retried events, double-emitted events.
- Contain: dedup key (
user+event+channel, TTL) + idempotency key on ingestion + provider idempotency where available → a retry within the window is a no-op. See dedup.
Sending after opt-out / during quiet hours (compliance failure)¶
- Impact: legal + trust violation (CAN-SPAM/TCPA).
- Contain: enforce preferences/opt-out/quiet-hours at fan-out, read fresh (cache with prompt invalidation on preference change). Opt-out must be immediate and auditable. Security/transactional override quiet hours by policy, never marketing.
Poison message (bad template/payload)¶
- Impact: a message fails every attempt, risks jamming a lane.
- Contain: cap retries → DLQ + alert; redrive after fixing. Don't loop forever on a permanent failure.
Invalid / stale push token¶
- Impact: push to a dead token fails.
- Contain: treat provider "unregistered/invalid" as permanent (4xx) — don't retry; prune the token from
devices. Lazy pruning driven by provider feedback keeps the token store clean.
Hard bounce / spam complaint (email)¶
- Impact: sending to bad addresses / complainers hurts sender reputation → providers may block you.
- Contain: auto-suppress the address on hard bounce/complaint (via the delivery-tracking webhook) so you never send to it again. Reputation is a shared, fragile resource.
Ingestion overwhelmed by a fan-out burst¶
- Impact: a broadcast or event storm floods intake.
- Contain: ingestion just accepts + enqueues (cheap); the queue buffers the burst; fan-out workers autoscale on queue depth. Stream recipient expansion in batches (never load 50M users at once). Backpressure/load shedding on non-critical intake if truly overwhelmed — but transactional intake stays protected.
Downstream status store slow/unavailable¶
- Impact: can't record delivery status.
- Contain: status writes are off the critical send path — buffer/queue them; a slow status store must not block or fail actual delivery. Worst case we lose some status granularity, not deliveries.
Preference/device cache stale or down¶
- Impact: fan-out can't read prefs → risk sending wrong things or stalling.
- Contain: cache falls through to the source store (degrade); fail safe on opt-out — if you truly can't confirm consent for a marketing message, don't send it; for transactional/security, proceed. Invalidate cache on preference change so opt-outs take effect promptly.
Region failure¶
- Impact: lose capacity/queues in a region.
- Contain: multi-region ingestion + queues; providers are global; the durable queue means accepted notifications aren't lost. Failover routing for ingestion.
Failure-handling toolkit used here¶
async ingestion (202) + buffered queues (accept even when downstream down) · circuit breaker + multi-provider
failover · provider rate limiting + pacing (429) · separate transactional/bulk lanes (priority) · dedup key +
idempotency (no double-send) · preference/opt-out enforced at fan-out (compliance) · retry+backoff → DLQ (poison)
· error classification (throttle vs transient vs permanent) · lazy token pruning + bounce suppression · stream
fan-out in batches · status writes off the critical path.
Priorities (say this)¶
"My guarantees: never miss a transactional notification, never double-send, and never send after opt-out. Async ingestion + buffered queues absorb provider outages (delayed, not lost); circuit breakers + multi-provider failover keep channels alive; separate transactional/bulk lanes keep OTPs fast during campaigns; dedup keys prevent duplicates; and preference/opt-out checks at fan-out keep us compliant. Everything degrades to delayed, not lost — except messages a user opted out of, which we must never send."
→ Next: Interview Questions