Skip to lesson
‹

Courses / System Design Interview / Chapter 35

Module 3 · Design Real Products · Chapter 35 of 39

Design a Notification System

20 sections · ~39 min✓ Fact-checked

Builds on Distributed Caches, Message Queues, Publish-Subscribe, Rate Limiters and Distributed Task Schedulers. Next up: Design a Ticket Booking System.

Part 1 · Requirements and Scale

The brief

Friday, 8:00 pm. A food delivery app sends a “50% off biryani” push to 20 million users. At 8:02, Priya in Pune logs in on her new phone; her OTP arrives at 8:06, after it has expired. She tries again, gets in and orders dinner for her parents. “Your rider has arrived” reaches her at 8:41, two minutes after Imran rang the bell. Nothing was down. The OTP, the order update and 20 million offers simply waited in one queue, in arrival order. Urgent messages needed a lane of their own.

You are building the notification system for that food delivery app, with about 20 million daily users. Any team should be able to send a push notification, an SMS, an email or an in-app message, from login OTPs and order updates to marketing offers. The urgent ones must arrive within seconds, even while a campaign is going out.

One service for every message

Priya’s evening went wrong because every message shared one path. A food delivery app sends many kinds of messages: a login OTP (one-time password) by SMS, “your order is out for delivery” as a push notification, a receipt by email, a weekend offer to millions, and a red dot on the in-app bell. If every team wired up its own SMS provider and push code, users would get duplicate, badly timed messages, and nobody could say what had been sent.

A notification system is one shared service that every team calls. It decides whether to send (preferences, quiet hours, limits), how (which channels) and what (the final text), then hands each message to its channel and tracks what happened. Each channel has its own path and its own trap:

ChannelDelivered throughBest forWatch out for
Pushthe phone platform’s push service (Apple’s for iPhones, Google’s for most Android phones)order updates, chat, remindersbest effort; needs a valid device token and the user’s permission
SMSSMS providers connected to telecom operatorsOTPs and critical alertscosts money per message; in India, business SMS needs registered sender ids and templates
Emailan email sending providerreceipts, statements, digestsspam filters; often read hours later
In-appour own inbox store, shown in the appa list of past messagesseen only when the app is opened
Push

Delivered throughthe phone platform’s push service (Apple’s for iPhones, Google’s for most Android phones)

Best fororder updates, chat, reminders

Watch out forbest effort; needs a valid device token and the user’s permission

SMS

Delivered throughSMS providers connected to telecom operators

Best forOTPs and critical alerts

Watch out forcosts money per message; in India, business SMS needs registered sender ids and templates

Email

Delivered throughan email sending provider

Best forreceipts, statements, digests

Watch out forspam filters; often read hours later

In-app

Delivered throughour own inbox store, shown in the app

Best fora list of past messages

Watch out forseen only when the app is opened

Only in-app is fully ours. Every other channel ends at an outside provider we cannot control, which is why lanes, retries and failover get the deep dives. Push needs one more idea: when the app starts, it gets a device token from the platform, an address for this app on this phone, and sends it to our servers. The requirements, in short:

Functional

Send a notification for a user and an event type

Pick channels from rules and user preferences

Templates with variables, in the user's language

Quiet hours, scheduled sends and campaigns

An in-app inbox with an unread count

Track sent, delivered, opened and clicked

Non-functional

OTPs and order updates out within seconds, even during a campaign

Never lose an important message; duplicates rare

Absorb bursts above 10,000 messages a second

Keep working when one provider fails

Out of scope

Building push or SMS delivery ourselves

The campaign design tool

Choosing who should get an offer

How many, how fast

Now size it. Assume the app has 20 million daily users, 5 million orders a day, and two marketing campaigns a day, each sent as a push to every one of those users.

Try it first: each order sends about 6 updates (placed, accepted, cooking, picked up, near, delivered), and OTPs, receipts and digests add about 10 M a day. What is the average send rate, and the rate while one campaign goes out over 30 minutes?
  • 30 M order updates + 40 M campaign pushes + 10 M others = 80 M a day, about 930 a second on average.
  • One campaign is 20 M pushes in 1,800 s, about 11,100 a second, plus about 1,000 a second of normal traffic: plan for about 12,000.
  • The peak is more than ten times the average, and it is planned: two campaigns a day.

The average hides the shape. A campaign is a burst we create on purpose, and the design must absorb it without delaying a single OTP behind 20 million offers. Storage and lookups, by contrast, are modest:

QuantityWorkingResult
Notification records80 M a day × about 500 B≈ 40 GB a day; ≈ 3.6 TB for 90 days, about 11 TB with 3 copies
Device tokens20 M users × 2 devices × about 200 B≈ 8 GB
Preferences20 M users × about 200 B≈ 4 GB: fits in a cache
SMSmostly OTPs, about 3 M a dayeach one costs money, so SMS only where push or email will not do

Nothing in that table strains a database. The hard number is the 12,000 sends a second of a campaign, and the rule that OTPs and order updates must not feel it.

Part 2 · API, Data and the Big Picture

The API

With the numbers known, define how teams call the service. They call one endpoint and get an answer at once; delivery happens later, and 202 Accepted makes that explicit. Here is Priya’s order update, and the other calls:

http
POST /v1/notifications
Idempotency-Key: order-81723-out-for-delivery
{
  "user_id": "u_42",
  "type": "order_update",
  "template": "out_for_delivery",
  "data": { "rider": "Imran", "eta_min": 7 },
  "deliver_by": "2026-10-16T20:45:00+05:30"
}
202 Accepted   { "notification_id": "n_9f3a" }

POST /v1/campaigns   { "segment": "inactive_30_days", "template": "weekend_offer", "send_at": "2026-10-18T12:00:00+05:30" }
PUT  /v1/users/u_42/preferences   { "offers": { "push": true, "sms": false, "email": true }, "quiet_hours": ["22:00", "08:00"], "tz": "Asia/Kolkata" }
POST /v1/devices    { "user_id": "u_42", "platform": "android", "token": "..." }
GET  /v1/users/u_42/inbox?cursor=...

The caller says what happened, for whom and which template; the service decides channels, timing and the final text for each channel and language.

Three fields carry the design:

  • type maps to rules: channels, lane, whether quiet hours apply. Callers do not pick channels themselves.
  • Idempotency-Key lets a caller retry after a timeout without Priya getting two messages.
  • deliver_by marks when the message stops being useful. An arrival estimate (ETA) from 40 minutes ago is noise, so it is dropped rather than sent late.

Services can also publish events such as “order picked up” to a topic and let the notification service subscribe, as in the pub-sub chapter. Then a notification outage delays messages instead of failing orders.

The data model

Behind the API, five tables hold the state. Four are read on almost every send and change rarely; the fifth, notifications, grows by tens of millions of rows a day.

usersuser_id (pk)phoneemaillanguagetimezonequiet_hours
preferencesuser_id (pk)category (pk)push, sms, email flags
devicestoken (pk)user_idplatformapp_versionlast_seen
templatestemplate_id (pk)version (pk)channel (pk)language (pk)bodysms_registration_id
notificationsnotification_id (pk)channel (pk)user_idtypetemplate_idtemplate_versiondedupe_keystatuscreated_atsent_atdelivered_atopened_at

notifications is written several times per message (queued, sent, delivered, opened), with one row per channel, keyed by notification id and channel. Partition it by user_id so one user’s history is a single range read, which is exactly what the in-app inbox needs, and expire rows after 90 days.

preferences and devices are read on every send, so keep them in a cache in front of the database. When a user turns offers off, delete their cached entry at once (the distributed cache chapter’s rule): one more offer after an opt-out is the kind of mistake users remember.

templates are versioned: an edit creates a new version, and each notification records the version it used, so you can always see the exact text that was sent. devices needs care: the app sends its token on every start, because the platform can replace it at any time; on logout the token is unlinked, so the next person on that phone does not get Priya’s orders; tokens the push service reports as unregistered are deleted at once.

Keep secrets out of storage and logs. Store the template id and its variables, with sensitive ones such as an OTP masked, rather than the rendered text, and never write a message body into logs. Support staff can still see which message went out and when, without seeing the code inside it.

The high-level design

Put the pieces together. The service accepts a request, runs it through a short decision pipeline, and drops one message per channel onto a queue for that channel. Channels that carry both kinds of traffic get two lanes: urgent (OTPs, order updates, receipts) and bulk (offers, digests). In-app messages need no provider, so the service writes them straight to the inbox store. Channel workers pull from the queues and call outside providers at a rate those providers allow.

Workers are pooled per channel because each channel has its own provider, limits and failure modes. Push workers can scale out for a campaign without touching the SMS workers that carry OTPs, and a slow email provider cannot hold up anything else.

For an OTP, our own part of the journey is small: checks and rendering take a few milliseconds, the urgent lane is usually empty, and the provider call takes a few hundred milliseconds. Most of the wait is the telecom network delivering the SMS, often a few seconds. A backed-up queue would be the one delay we caused ourselves.

Two questions remain before the deep dives: what the pipeline decides for each message, and how lanes keep a campaign away from Priya.

Deciding what to send

Every request passes through the same short pipeline before anything is queued. Each step can stop the message, delay it, or change its channel:

Inside the notification service
  1. 1Check the calleris this service allowed to send this type?
  2. 2De-duplicatekey seen in the last day? return the first result and stop
  3. 3Load the userpreferences, devices, language, time zone, all cached
  4. 4Pick channelsthe type's default channels minus the user's opt-outs
  5. 5Check timing and limitsquiet hours, per-user caps, deliver_by
  6. 6Renderfill the template in the user's language
  7. 7Enqueue and recordone message per channel, status queued

The rules per type live in one table, owned by the notification team:

TypeChannelsLaneQuiet hoursPer-user cap
Login OTPSMSurgentignored3 per 10 minutes per phone number, against abuse
Order updatepush, in-appurgentignorednone
Payment receiptemail, in-appurgentignorednone
Offerpush, email, in-appbulkrespected2 pushes a day
Weekly digestemailbulkrespected1 a week
Login OTP

ChannelsSMS

Laneurgent

Quiet hoursignored

Per-user cap3 per 10 minutes per phone number, against abuse

Order update

Channelspush, in-app

Laneurgent

Quiet hoursignored

Per-user capnone

Payment receipt

Channelsemail, in-app

Laneurgent

Quiet hoursignored

Per-user capnone

Offer

Channelspush, email, in-app

Lanebulk

Quiet hoursrespected

Per-user cap2 pushes a day

Weekly digest

Channelsemail

Lanebulk

Quiet hoursrespected

Per-user cap1 a week

Urgent types skip quiet hours and caps; bulk types obey both. Fallbacks live in the same table: if an order update cannot go by push, because Priya has no device token or has push switched off, steps such as “your rider has arrived” fall back to SMS. Offers never fall back to SMS. A new kind of notification is a new row, reviewed once, which is what stops a well-meaning team from sending offers at 2 am.

Part 3 · Deep Dives

Urgent and bulk lanes

Core deep dive

Lanes are the heart of this design: without them, one campaign delays every OTP and order update behind it.

Back to Friday, 8 pm. A queue hands out messages roughly in arrival order, so in one shared queue Priya’s update would wait behind 2 million offers for about 3 minutes. Lanes work like the separate counter at a railway booking office: the long general line never reaches it. Priority must hold at every shared stage, or the line forms at the next one:

  • Intake: the campaign job feeds the pipeline in paced batches, so offers never pile up in front of an OTP request.
  • Queues: each channel that carries both kinds has an urgent lane and a bulk lane.
  • Workers: each lane has its own workers, so offers cannot occupy all of them.
  • Provider quota: workers share one rate limit per provider account, and the urgent lane keeps a reserved share.
Predict before you watch: 2 million offers wait in the bulk lane when Imran picks up Priya’s order. How long does her push wait?
  • Well under a second: the urgent lane is empty, with its own workers and quota.
Animated sceneAn order update passes a campaign0:19

8 pm: the campaign job feeds offers in paced batches, and 2 M wait in the bulk lane

Read it as steps

Watch for Priya’s order update: 2 million offers wait in the bulk lane, but the update has its own lane, workers and share of the quota.

Lanes make nothing faster; they stop a patient burst from delaying impatient messages. A campaign may delay other offers, never an OTP or an order update.

Size the reserved share from numbers. Say the push provider allows this account 15,000 sends a second (real quotas are agreed with each provider). Cap bulk at 11,000, enough for a 20 M campaign in 30 minutes, and keep 4,000 for urgent, about four times the dinner-peak rate of order updates. The cost is idle quota on quiet evenings.

Retries and failover

Lanes keep Priya’s messages from waiting on us. Providers fail in their own ordinary ways: timeouts, 5xx errors, throttling with 429, and permanent rejections such as an invalid number or a device token from an uninstalled app. The worker treats each differently:

Provider responseWorker action
Acceptedmark sent, then acknowledge the queue message
Timeout or 5xxretry with exponential backoff and jitter (about 1 s, 2 s, 4 s), up to an attempt limit or deliver_by
429 throttledslow down for this provider; retry after the time it asks for
Token not registereddelete the token; do not retry
Invalid number or emailmark failed; flag the contact so the user can fix it
Retries used upmove to a dead-letter queue and alert

Only temporary failures are retried; retrying a permanent rejection wastes money and quota. Each SMS and email provider also sits behind a circuit breaker: if its error rate stays above a threshold, say 20% over a minute, workers stop calling it and use the standby provider.

Animated sceneAn OTP fails over to a second provider0:18

Priya’s OTP goes to provider A, the primary, which gives no answer in 2 s

Read it as steps

Watch for the circuit breaker: once provider A keeps failing, no OTP waits on it, and test sends bring it back later.

Two rules made Priya’s OTP fast. An OTP’s retry goes straight to the other provider, because backoff against a failing provider spends a waiting user’s patience. And the breaker spares everyone else the timeouts. Push has no standby, since each platform has one push service: pushes wait until their deliver_by, and critical steps fall back to SMS.

Duplicates

Retries and failover keep messages moving, but every retry risks a second copy. The queue delivers at least once (see the message queue chapter): a worker can call the provider, then crash before acknowledging, and another worker receives the same message. So de-duplication happens in layers, each an atomic check, such as a set-if-absent with a one-day expiry, so two copies arriving together cannot both pass.

Animated sceneA redelivered OTP0:19

Watch for the status check: worker 1 asks it first, and worker 2 asks it again

Read it as steps

Watch for the status check: worker 1 saved ‘sent’ before it crashed, so worker 2 skips the provider.

The status check turns a redelivery into a no-op. One window stays open: a crash after the provider accepted the message but before the status was saved. The status check is one of four layers:

LayerKeyStops
APIIdempotency-Key from the callera caller retrying after a timeout
Eventuser + event + order id, such as u_42:out_for_delivery:81723the same event published twice, or redelivered by the topic
Workerthe message’s status (notification id and channel), checked before calling the providerredelivery after a worker crash
Devicea collapse id, so a newer push replaces the older onea stack of stale ETAs on the lock screen

Each layer catches a repeat the others cannot see.

One duplicate can still slip through

If a provider accepts a message and its reply is lost, as with Priya’s OTP at provider A, the worker cannot know it was sent. Retrying risks a duplicate; not retrying risks a loss. For OTPs and order updates, prefer the rare duplicate: the login service creates the code once, so a repeat carries the same code. Where a provider accepts an idempotency key, pass the notification id.

Part 4 · Failures and Trade-offs

What can fail

Most failures here start somewhere else: a provider slows down, a phone platform rejects tokens, a caller floods the API. The design’s job is to keep each failure inside its own lane:

FailureEffectHandling
An SMS provider goes downOTPs stallthe circuit breaker opens; OTPs go through the standby provider
The push service is slow or downpushes arrive lateretry until deliver_by; critical steps fall back to SMS
A campaign floods the systemthe bulk lane backs upthe urgent lane, its workers and its quota are untouched
The notification service is downdirect calls failevent publishers lose nothing; direct callers retry with the same idempotency key
The preferences store is downopt-outs cannot be checkedserve from cache; if unknown, send urgent messages and hold bulk ones
A bad template shipsbroken text to millionsnew versions go to 1% first; a kill switch per template and campaign
A zone or region failsa zone takes some workers down; a region stops all sendingqueues and stores span three zones; a standby region restarts OTPs and order updates first while campaigns pause, accepting a few duplicates from lagging de-dup records
A deploy restarts the fleetin-flight messages are redelivered; apps with a live in-app connection reconnectdrain workers a few percent at a time, never during a big send; the status check absorbs redeliveries; apps reconnect with jitter

In every row the urgent path survives: failures delay bulk work or cost a rare duplicate, but never strand an OTP behind someone else’s problem.

The kill switch

Every campaign and every template needs an off button that works within seconds. Sending 20 million wrong messages cannot be undone; stopping after 50,000 is a much better day.

Trade-offs and running it

Each choice above trades something away. Here are the main ones, with the usual pick:

DecisionOptionsUsually pick
Delivery guaranteeat most once, or at least onceat least once, with layered de-duplication
Queuesone shared queue, or per channel and priorityper channel, with urgent and bulk lanes
Quiet hoursdrop, or delaydelay, unless deliver_by has passed
How callers reach usdirect API calls, or events on a topicevents for business changes; the API for direct requests such as OTPs
Providersone per channel, or twotwo for SMS and email; push has one service per platform

Every pick favours the urgent message over the cheap or simple option. Three signals show whether that is working:

What dominates the bill: SMS, about 3 M a day, each one paid for, while push is free to send and email costs a small fraction per message. That is why OTP templates stay short and offers never fall back to SMS.

Part 5 · Going further

The burst that cannot be spread

Optional: four further topics, starting with a burst that the bulk lane cannot absorb.

Campaigns are big but patient: an offer at 8:25 is as good as one at 8:00, so the bulk lane drains it over half an hour. Some messages lose their value in minutes. During the cricket World Cup final, the app runs an innings-break deal, “free dessert for the next 15 minutes”, and must tell 10 M users within 2 minutes, or the break is over.

That is 10 M ÷ 120 s ≈ 83,000 sends a second, about seven times the campaign rate, and none of it can be smoothed out. At the bulk lane’s 11,000 a second it would take 15 minutes. It needs its own preparations:

ToolWhat it doesWhy it helps
Pre-sharded audience listsBefore the match, keep the audience’s device tokens and languages in shards of about 10,000No audience query at send time; each shard is one independent job
Many workers, warmed upStart them before the break, since scaling up takes minutesAt about 1,000 sends a second each (an assumption), 100 workers cover 83,000 with some room
One render per languageFill the template once per language, not once per userTen renders instead of ten million
Agreed provider quotaRaise the push quota with the provider well in advance, and load-test togetherThe normal 15,000 a second is the ceiling otherwise

The quota is the real limit: workers are easy to add, and 83,000 a second is more than five times the account’s normal quota. Keep the safety rules while you go fast. The urgent lane keeps its workers and reserved quota, so Priya’s order updates still flow; each deal push carries a deliver_by 2 minutes out and a collapse id. Where the platform offers topics, one send can reach every subscribed device, at the cost of per-user status and caps.

Preferences, quiet hours and limits

The next topic is how users stay in control. Preferences are stored per category and channel: Priya can keep order updates on push but turn offers off everywhere. Some messages ignore preferences because the user asked for them or rules require them, such as OTPs and security alerts. Marketing email must carry a working unsubscribe link: laws in many countries require one, and large email providers expect one-click unsubscribe from bulk senders.

Quiet hours are kept in the user’s own time zone. A bulk message that falls inside them is not dropped straight away: the task scheduler (see the task scheduler chapter) holds it until quiet hours end, unless its deliver_by will have passed by then. Held messages are released over the first hour of the morning, so 8:00 does not become a second spike. Here is Priya’s evening; her quiet hours run from 10 pm to 8 am:

TimeMessageDecisionWhy
9:40 pmWeekend offersend nowbefore quiet hours, under her daily cap
10:30 pmFlash sale, ends 11:59 pmdropquiet until 8 am, and useless by then
10:45 pmWeekly digesthold until after 8 amstill useful tomorrow
11:10 pmLogin OTPsend nowOTPs ignore quiet hours
11:20 pmLate snack: out for deliverysend nowshe is awake and waiting for food
9:40 pm

MessageWeekend offer

Decisionsend now

Whybefore quiet hours, under her daily cap

10:30 pm

MessageFlash sale, ends 11:59 pm

Decisiondrop

Whyquiet until 8 am, and useless by then

10:45 pm

MessageWeekly digest

Decisionhold until after 8 am

Whystill useful tomorrow

11:10 pm

MessageLogin OTP

Decisionsend now

WhyOTPs ignore quiet hours

11:20 pm

MessageLate snack: out for delivery

Decisionsend now

Whyshe is awake and waiting for food

Quiet hours delay only what can wait; anything Priya is actively waiting for goes at once.

Per-user limits reuse the rate limiter chapter’s counters, kept in a shared in-memory store with atomic increments: a daily count per user and category, such as 2 offer pushes a day, and a short window per phone number for OTPs. An offer over its cap is dropped, not saved for later. Limits protect users who still have notifications switched on: someone who disables push because of spam stops getting order updates too.

Templates, languages and campaigns

Next, the words themselves. A template is a message with named gaps that the service fills at send time from the request’s data. Each template has a variant per channel, because a push line, an SMS and an email need different lengths and tone, and a variant per language, falling back to English if one is missing:

text
template: out_for_delivery    version: 7

push, en:  Your order is on its way. {rider} will reach you in about {eta_min} min.
push, hi:  आपका ऑर्डर रास्ते में है। {rider} लगभग {eta_min} मिनट में पहुँचेंगे।
sms,  en:  Order on its way. {rider} arrives in about {eta_min} min.

data:      { "rider": "Imran", "eta_min": 7 }
push, en → Your order is on its way. Imran will reach you in about 7 min.

One template, with a variant per channel and language. Variables are filled in at send time.

Mind each channel’s limits. One SMS holds 160 characters of the basic GSM alphabet (GSM-7): English letters, digits and common punctuation. One character outside it, such as a Hindi letter, an emoji or the ₹ sign, switches the whole message to UCS-2, a 16-bit encoding that fits only 70. Longer texts are split into parts of 153 or 67 characters, and every part is billed, so keep OTP templates short in every language. Push payloads are capped at about 4 KB, and the lock screen shows a line or two.

Campaigns are many small sends. A campaign job reads the audience in batches of, say, 1,000 users and passes each through the normal pipeline, so their own preferences, caps and quiet hours apply. Workers check the campaign’s kill switch before every send, so stopping a campaign also stops messages still waiting in the lane. Three rules keep templates safe:

  • Missing variable? Fail that send and alert. Never deliver Hello {name} to anyone.
  • Escape per channel. Data placed into an email must be HTML-escaped, so an odd restaurant name cannot break the layout or slip in a link.
  • Test before launch. Preview every language, and give new versions to a small share of users first.

Tracking delivery and opens

Last, how do you know what happened to each message? Every message moves through statuses, and each status comes from a different place with different reliability. Providers report delivery and bounces later, by calling a webhook of ours:

StatusWho tells usHow reliable
queued, sent, failedthe notification service; the provider’s answerexact
deliveredSMS reports from operators; for email, the receiving server accepting it; for push, the app reporting arrivalgood for SMS; an accepted email can still land in spam; partial for push
openedthe app reports a tap with the notification id; email opens come from a tiny tracking imagepush taps are reliable; email opens are not, since mail apps block or preload images
clickedlinks pass through our redirect, which records the clickgood

Treat email opens as a rough signal and SMS delivery reports as the dependable one. Status events go onto their own queue and into an analytics store, away from the hot send path, so dashboards can answer whether OTP delivery is dropping for one operator or yesterday’s campaign raised opt-outs.

Tracking also feeds back into sending. Email addresses that hard-bounce go on a suppression list and dead device tokens are deleted, so we stop paying for messages nobody can receive. The in-app inbox is the simplest channel: write a row for the user, bump an unread counter, and let the app fetch the list with a cursor; opening an item marks it read.

Part 6 · Interview kit

The 45-minute answer

A 45-minute round leaves about 40 minutes of design after introductions. Here is how this design fits that clock:

MinutesWhat you say and drawKey number or decision
0–5Requirements: four channels, preferences and quiet hours, templates, campaigns, tracking. Non-functional: OTPs within seconds even during a campaign; important messages never lostthe OTP-during-a-campaign requirement shapes everything
5–8Estimates that decide something: 80 M sends a day, about 930 a second on average, about 12,000 a second during a campaignpeak is more than 10× the average, on purpose
8–13API: POST /notifications with type, template, data, an idempotency key and deliver_by, answered with 202. Tables: preferences, devices, templates, notifications partitioned by usercallers say what happened; the service picks channels
13–22Draw: the pipeline, one queue per channel with urgent and bulk lanes, workers, providers, the in-app inboxper-channel queues and workers
22–37Deep dive: lanes held at every shared stage with a reserved quota; then failover behind circuit breakers and layered de-duplicationkeep 4,000 of 15,000 pushes/s for urgent; fail over at about 20% errors
37–40Wrap-up: provider and region failures, kill switches, what you would monitor, SMS as the main costa kill switch that works within seconds

Lanes carry most of the marks, because they answer the requirement that shapes everything. If they ask what happens when the SMS provider goes down, or whether a user can get two OTPs, go to failover and duplicates; for an innings-break deal that must reach 10 M users in 2 minutes, go to the unspreadable burst.

Say
“One queue per channel, with urgent and bulk lanes, their own workers and a reserved share of the provider quota, means 2 M queued offers never delay an OTP.”
Say
“Delivery is at least once: workers check the status before sending, so redeliveries are skipped. A rare duplicate OTP beats a lost one.”
Say
“SMS costs money per message, so it carries OTPs and critical fallbacks only; offers go by push, email and in-app.”
Avoid
“The order service calls the SMS provider directly.” Every team then rebuilds retries, limits and opt-outs, and gets them wrong.
Avoid
“We guarantee exactly-once delivery.” A provider can accept a message and lose the reply; aim for at least once with layered de-duplication.

Interview drill

Interviewers often change one thing and watch how you adapt. Try each before opening the answer.

Cricket final night: between innings, order updates jump 10×, from about 500 to 5,000 a second, for half an hour. What do you change?
  • Scale the urgent push workers before the match; autoscaling reacts in minutes, and this spike is on the calendar.
  • The reserved 4,000 a second is now too small: pause campaigns for that hour so the whole quota is free, and ask the provider for headroom in advance.
  • The in-app inbox and the de-dup store see a few times 5,000 writes a second, which their clusters handle.
Product wants one live order card that updates in place instead of six separate notifications. What changes in the data model?
  • One record per order, not per message: an activity keyed by order id with a version number, updated in place in the inbox.
  • Every update carries the version, and apps ignore anything older than what they show, since retries deliver out of order.
  • Push uses the order id as the collapse id, so the platform keeps only the newest; cap updates to one every 15 to 30 seconds.
Finance asks you to halve the SMS bill without hurting logins. What do you change?
  • Where the user is logged in on another device, send the code there by push or in-app first, and fall back to SMS only if it goes unused for a few seconds.
  • Keep every SMS template within one part: 160 GSM-7 characters, or 70 once a Hindi letter or emoji appears.
  • Split traffic across providers by price and delivery rate per operator, and stop SMS fallback for order steps that are not critical.
Common miss
Lanes in the queue but one shared provider rate limit: bulk workers use it all, and order updates get 429s.
Common miss
No deliver_by on time-bound messages, so a stale ETA arrives after the food.

Wrap-up

The whole chapter on one page, for revision.

Next, the ticket booking chapter, Design a Ticket Booking System, sells 40,000 seats to 10 lakh fans at noon, and sends each ticket through a service like this one.

Part 7 · Check yourself

Quick check

Q1During the 8 pm campaign, Priya’s order-update pushes arrive minutes late, yet the urgent push lane is nearly empty. What is going on, and what is the fix?

Think it through, then check.

Q2Product adds a business chat channel for order updates, with pre-approved templates and its own provider. What changes?

Think it through, then check.

Q3A teammate says workers send, then mark sent, so every message goes exactly once. Spot the flaw. After a lost reply, should a worker retry an OTP, and a weekend offer?

Think it through, then check.

Q4At 8 pm, mid-campaign, the whole region running the notification service fails. What do users notice, and what should come back first?

Think it through, then check.

Q5Quiet hours hold Priya’s weekly digest until morning. Which building block keeps it, and why not leave it waiting in the bulk lane?

Think it through, then check.

Next chapter · 36 of 39Design a Ticket Booking System~41 min · 2 animated scenes