Three channels, three characters
Three channels sit under this system, and they have wildly different characters.
Push notifications go through the phone platforms, cost almost nothing, arrive almost instantly, and die silently when a device token goes stale.
Email costs fractions of a penny, tolerates minutes of delay, and lives under deliverability rules where one bad campaign lands your domain on a blocklist.
Text messages cost actual pennies each, which turns every volume decision into a budget decision.
Front all three with one system, because the cross-cutting concerns are identical. Preferences, retries, rate limits, auditing. No company wants those reimplemented per team.
Size it. A hundred million people receiving five notifications a day is 500 million sends, about 5,800 a second sustained.
Distrust that average, because notification traffic is bursty by nature. A campaign to 20 million people, dropped in at 9am, wants to go out now.
Do the arithmetic on that burst. If your pipeline drains 30,000 a second, that one campaign occupies it for eleven minutes, and everything behind it in the queue waits. That is why this architecture is built around queues and why priority lanes show up later.
The contract
Make the API contract explicit, because your callers will live with it for years. They send an event: who it is for, which template, the variables, a priority, and a key that identifies this request uniquely.
Keep three things away from them. Raw text, because templates keep your copy centrally managed and translatable. Channel choices and device tokens, because channel selection belongs here, in the only place your preferences and token registry live.
Accept asynchronously. Validate, enqueue, and return an identifier the caller can query for status. Promising synchronous delivery is a promise this system cannot keep, because the gateways you depend on fail on their own schedule.
Worked example
Ravi joins a food delivery company where four teams send notifications independently: orders calls FCM directly, marketing uses a SaaS tool, payments calls Twilio inline (a payment once failed because Twilio was slow and the call was synchronous), and the driver app has its own push code. The visible symptom is a support ticket: a user got 11 notifications in one dinner order, including three about the same delayed driver. Nobody can answer the basic question of what was sent to this user today, because there is no single log. Ravi's pitch for a central platform is one slide of arithmetic: 4 teams x 4 duplicated concerns (tokens, retries, preferences, auditing) versus one pipeline with an events API. The payments incident seals it, because moving Twilio behind a queue makes payment latency independent of SMS gateway health.