Notifications, where the password reset must not wait behind the newsletter
Sending a message is easy. Sending the right one, once, on the right channel, while a marketing campaign of ten million is in the same pipes, is the design.
The brief
Other services ask this system to notify a user — an order shipped, a login code, a weekly digest — and it delivers by push, email or SMS through external providers.
Users control which notifications they receive on which channel.
Requirements
Functional
- Accept a notification request for a user, with a type and data
- Choose channels from the type and the user's preferences, and render a template
- Deliver through external providers, with retries on failure
- Record what was sent, for support and for 'did the user get it?'
Non-functional
- A one-time login code must arrive within seconds, whatever else is queued
- Duplicates are tolerable for a shipping update and unacceptable for a message that looks like a charge
- An unsubscribe must take effect before the next send, not the next day
- A provider outage must degrade one channel, not the whole system
Back-of-envelope
Assume
- 20 million users, 5 transactional notifications per user per week
- One marketing campaign a day to 10 million users
- Login codes: 2 million a day, peaking at 5× the average rate
- The SMS provider accepts 200 messages a second on this account
Therefore
- Transactional: 1e8 a week ≈ 14 million a day ≈ 165 a second on average. Small.
- A campaign: 10 million messages. Sent as fast as possible, it is thousands a second for about an hour — a single campaign is more traffic than a week of everything else in its first hour.
- Login codes: 2e6 / 86,400 ≈ 23 a second, ≈ 115 a second at peak. That is the traffic with the deadline, and it is a tiny fraction of the total.
- At 200 SMS a second, a campaign of 1 million SMS takes 5,000 seconds — about 83 minutes — of the entire SMS allowance. If login codes share that allowance, every code sent during those 83 minutes waits.
The last line is the design. Nothing about total volume is hard; what is hard is that the smallest, most urgent traffic shares a rate-limited provider with the largest, least urgent.
The interface
What is stored
notification_id · user_id · type · idempotency_key UNIQUE · created_atThe unique key is where duplicate requests stop. It is checked on insert, so two identical requests arriving at the same moment cannot both pass.
delivery_id · notification_id · channel · status · attempts · provider_message_id · INDEX (user_id, created_at)One row per channel attempt, because a notification can succeed by push and fail by email. provider_message_id is what joins a provider's delivery-report webhook back to this row.
user_id · type · channel · enabledSmall and read constantly, so it is cached — with the cache invalidated on write rather than expired on a timer, for the unsubscribe reason above.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- A campaign to ten million users starts at 9:00. A user requests a login code at 9:05. Trace the code's path and say how long it waits.
- A user unsubscribes and receives the next campaign anyway. Which component let that happen?
- The provider times out on a send. Do you retry? What decides it, per message type?
- Where does exactly-once break down, and what do you promise instead?