← Back to practice catalog

System design interview guide

Notification System Design

TL;DR:

A product sends password reset codes, receipts, marketing campaigns, and urgent alerts through email, SMS, and push. A marketing blast must not delay password resets, providers rate-limit traffic, and retries must not spam users with duplicates.

Overview diagram for Notification System Design

Problem statement

Design a notification service that accepts events from product services, renders the right template, respects preferences, fans out to email, SMS, push, and in-app channels, retries provider failures, and protects urgent transactional messages from noisy campaigns.

Introduction

You request a password reset code. The app says the code was sent. Five minutes pass. You request again. Now two codes arrive, and the first one is already invalid. From the user's view, the notification system failed, even if the API returned 202 and the provider accepted the request.

A notification platform is a delivery system wrapped in product rules. It must decide what to send, which channel to use, whether the user allowed that channel, which template and language to render, how often to retry, and what to do when providers slow down. It also has to protect urgent messages from noisy ones. A receipt or OTP should not sit behind a marketing blast.

The biggest interview trap is claiming exactly-once delivery. You can make your internal processing idempotent. You can dedupe logical notifications. You can record attempts. But you cannot guarantee that a human sees exactly one alert across email inboxes, phones, carrier networks, provider retries, and app reinstalls. A strong design says the honest promise: durable enqueue, at-least-once processing with dedupe, clear suppression rules, and observable delivery attempts.

How to approach

Split the system into three phases before naming infrastructure: ingest, route, and deliver. Ingest saves the request and returns quickly. Routing applies product policy. Delivery talks to providers with retries and limits.

Clarifying Q&A

You: "Are we handling transactional messages, marketing campaigns, or both?"

Interviewer: "Both, but transactional messages such as OTP and receipts must be protected."

That answer means you need separate lanes or priority budgets. One marketing campaign must not delay password resets.

You: "Which channels are in scope: push, email, SMS, and in-app?"

Interviewer: "Use push, email, and SMS. Mention in-app if it helps reliability."

Now you should draw channel-specific queues and provider adapters, not one generic sender.

You: "Do preferences and quiet hours apply to every notification?"

Interviewer: "Marketing must respect them. Some transactional messages may bypass quiet hours."

This tells you preferences are policy, not a simple boolean. Category matters.

You: "Should POST wait until the provider delivers, or return after the job is saved?"

Interviewer: "Return after saving and enqueueing. Delivery is async."

That sets the API contract: 202 means accepted for processing, not delivered to the user's device.

You: "What should we promise about duplicates?"

Interviewer: "Avoid user-visible duplicates, but be realistic."

Now you can explain idempotency keys and dedupe without pretending the outside world is exactly once.

Minute pacing + In the room

MinutesFocus
0-5Clarify categories, channels, preferences, delivery promise, and duplicate tolerance.
5-12Capacity, HLD, and queue separation.
12-24Detailed design for ingest, routing, template, delivery, retry, and status.
24-34Architectural dig on channel fan-out, dedupe, and provider rate limits.
34-45Failure, observability, security, cost, bottlenecks, and interview pushes.

In the room: "I will make POST durable and fast: validate, dedupe, save, enqueue, return 202. Router workers load preferences, template, locale, and category, then create channel jobs only for allowed destinations. Channel workers send through provider adapters with per-provider rate limits and retries. I will keep transactional and marketing lanes separate, put poison messages in DLQ, and be clear that exactly-once to a human is not a promise."

Out of scope

Building the external providers. APNs, FCM, SMS carriers, and email services already exist. The design integrates with them and handles their limits.

A full marketing campaign builder. Audience selection and campaign analytics can be separate. This service receives a notification request or campaign batch and delivers safely.

Long-term business intelligence. Delivery events can stream to analytics, but the interview should focus on operational status, retries, and correctness.

Perfect human attention tracking. You can track provider status, app open, and click events, but you cannot prove the user read the message.

Capacity estimation

Notifications have bursty traffic and uneven importance. A password reset and a sale announcement may share providers, but they should not share the same failure budget.

DimensionExample scaleWhat it means
Daily sendsHundreds of millionsQueues and workers must be horizontally scalable.
Campaign burstMillions in minutesBatch fan-out and throttle to provider quotas.
Transactional latencySeconds, not minutesIsolate OTP, security, and receipt lanes.
Device tokensMany per user, many staleToken registry needs cleanup on provider errors.
Templates and localesThousands of variantsVersion templates and validate payload variables.
Provider limitsPer account, region, domain, or numberRate limit per provider and tenant, not only globally.

This pushes the design toward durable queues, channel-specific workers, and backpressure. Adding more workers is not always correct; if the provider allows only a fixed send rate, extra workers just create retries and cost.

High-level architecture

The system has an ingestion layer, a routing layer, channel queues, provider adapters, and status storage. Producers do not call Twilio or FCM directly. They call the notification API with a logical notification request.

The Notification API validates the caller, checks idempotency, stores a notification row, and enqueues a routing job. Router workers load user preferences, device registry, template version, locale, and category. They suppress disallowed messages, schedule messages for quiet hours, or create channel jobs. Channel workers send through provider adapters, enforce rate limits, record attempts, and retry transient failures. DLQ tooling holds poison jobs that should not be retried forever.

[ Product services ]
        |
        v
[ Notification API ] -> [ Notification DB ] -> [ Routing queue ]
        |                                           |
        |                                           v
        |                              [ Router: prefs + template ]
        |                                           |
        |                 +-------------------------+-------------------+
        |                 v                         v                   v
        |          [ Push queue ]             [ Email queue ]      [ SMS queue ]
        |                 |                         |                   |
        v                 v                         v                   v
[ Status API ]     FCM / APNs adapters        SES / SendGrid       Twilio / carrier

DLQ and replay tools sit beside each queue.

In the room: Say that 202 from the API means the notification was saved and queued. It does not mean a provider delivered it, and it certainly does not mean the user saw it.

Core design approaches

The most important approach is lane isolation. Transactional messages such as OTPs, password resets, security alerts, receipts, and booking confirmations need a faster and more protected path than marketing campaigns. That can be separate queues, strict priorities, or per-lane worker budgets. The key is that a campaign cannot consume all provider quota while OTPs wait.

The second approach is policy before fan-out. Do not create email, SMS, and push jobs for every event and then filter later. Load preferences, category, quiet hours, locale, and template metadata first. If the user opted out of marketing SMS, the system should write SUPPRESSED and stop before paying for an SMS job.

The third approach is at-least-once with dedupe. Internal queues usually deliver messages at least once. Workers can crash after sending but before acknowledging. Providers can timeout after accepting a message. Instead of disabling retries, use a dedupe key for the logical notification and keep attempt records. Accept that rare duplicates may still happen outside your boundary, and design the client or in-app inbox to collapse duplicates where possible.

Data model

RecordImportant fieldsWhy it exists
notificationnotification_id, producer, user, category, dedupe_key, statusLogical notification request and idempotency anchor.
templatetemplate_id, version, locale, channel, required variablesKeeps rendering deterministic and reviewable.
preferenceuser, channel, category, quiet hours, versionDecides whether a message may be sent.
destinationdevice token, email address, phone, verified status, last_seenTells workers where to send and when to stop trying.
channel_jobnotification id, channel, priority, scheduled_at, attempt_countWork item for a specific channel.
delivery_attemptprovider, provider message id, status, error code, latencyExplains what happened with each provider call.
suppressionuser or destination, reason, source, created_atPrevents sends after hard bounce, opt-out, or invalid token.
dlq_messageoriginal job, error, first_seen, last_seen, replay statusHolds poison work for investigation.

Store dedupe_key as a uniqueness rule per producer or tenant. A useful shape is producer_event_id + user_id + notification_type. If the producer retries the same logical event, the API returns the existing notification id.

Detailed design

The user-facing API should be fast because it is usually called from another product service during a user action. When checkout completes, the checkout service should not wait for email, SMS, or push providers. It should ask the notification service to send a receipt, receive 202 Accepted, and continue.

On ingest, the Notification API authenticates the producer service, validates the payload against the chosen template contract, checks the idempotency or dedupe key, and saves a notification row. If the same logical notification already exists, the API returns the existing id and current status instead of creating a duplicate. Then it enqueues a routing job and returns 202. This is the point where the producer's synchronous responsibility ends.

A router worker picks up the routing job and loads the user's preferences, available destinations, category rules, and template metadata. If the message is marketing and the user opted out of email, the router records SUPPRESSED for email. If quiet hours apply, it schedules the job for the next allowed time. If the message is transactional, it may bypass quiet hours but still respect hard legal suppressions such as unsubscribed marketing email or invalid phone numbers, depending on the rule. The router renders or prepares template variables with a specific template version so retries do not accidentally change wording halfway through.

Fan-out happens after policy. If the notification can use push and email, the router creates separate channel jobs. Push may fan out further to multiple device tokens for one user. Email may choose a verified email address. SMS may choose a phone number and country-specific provider. Each job carries priority, scheduled time, dedupe key, and enough template context for the worker to send without calling every upstream service again.

Channel workers send with provider-specific rules. Push workers talk to FCM or APNs and delete stale tokens when providers return terminal errors such as unregistered token. Email workers respect domain reputation, bounce suppressions, and template rendering rules. SMS workers respect per-number, per-country, and carrier throughput limits. All workers use exponential backoff with jitter for transient 429 and 5xx errors. They do not retry permanent template errors forever; those go to DLQ with enough detail for a person to fix and replay.

Provider failover is useful but not magic. If SendGrid is down, SES might help only if the sending domain, DKIM, template, unsubscribe header, and reputation setup are ready. If one SMS provider is throttled in a country, another provider may have different compliance or sender-id rules. Failover should be configured by channel and region, and it should preserve dedupe so the user does not receive the same OTP twice from two vendors.

Status is written throughout the journey. A notification can be ACCEPTED, SUPPRESSED, QUEUED, SENT_TO_PROVIDER, DELIVERED_BY_PROVIDER, BOUNCED, FAILED, or DLQ. These names should not overpromise. DELIVERED_BY_PROVIDER means the provider says it delivered to a device or mailbox endpoint. It does not mean the human read it.

In the room recap: "POST validates, dedupes, saves, queues, and returns 202. Router checks preferences, quiet hours, template, locale, and destinations before creating channel jobs. Channel workers enforce provider limits, retry transient errors, record attempts, remove dead tokens, and send poison messages to DLQ. Transactional and marketing lanes are isolated, and exactly-once to the human is not the promise."

Architectural dig

One logical notification across three channels

The hardest part is not making one HTTP call to a provider. It is keeping one logical notification consistent while it fans out to several unreliable channels. A weak design lets every producer call every provider directly. Then preferences are inconsistent, retries duplicate messages, and marketing can drown transactional traffic.

Bad design

Checkout service -> SendGrid directly
Auth service     -> Twilio directly
Marketing tool   -> FCM directly

Result: no shared dedupe, no preference audit, no lane control, no common retries

The better design accepts one logical notification, then fans out after policy.

Great design

POST notification(dedupe_key, user, template, category)
  -> save notification row
  -> router loads prefs + template + destinations
  -> create allowed channel jobs
  -> channel workers send with rate limits and retries
  -> attempts update one status trail

One logical notification routes through preferences and templates, then fans out to push, email, and SMS lanes with rate limits

Dedupe lives at more than one level. The API dedupes producer retries using dedupe_key. The router should not create duplicate channel jobs for the same notification-channel-destination tuple. Channel workers should store provider message ids when providers return them. The mobile app or in-app inbox can collapse notifications with the same logical id if a provider retry still creates a duplicate visible alert.

Provider rate limits are also part of the design, not an ops footnote. Use token buckets per provider account, tenant, channel, and priority. If Twilio returns 429, do not let every SMS worker retry immediately. Pause or slow that provider lane with jitter, protect transactional jobs first, and let marketing backlog or shed according to product rules.

Key challenges

ChallengeWhat users or teams seeStrong answer
Channel fan-outUser receives too many or too few messagesRoute after preferences and dedupe per channel destination.
PreferencesOpted-out users still receive marketingCheck preferences before fan-out and audit preference version.
Duplicate producer eventsSame receipt or OTP sent twiceDedupe key on logical notification and channel job.
Provider retriesWorker sends, times out, retries, and duplicatesAttempt log, provider id, dedupe, and client/inbox collapse.
Provider limitsQueue age grows and 429s explodePer-provider token buckets and backoff with jitter.
Poison templatesOne bad payload blocks a partitionDLQ with alert and replay after fix.
Exactly-once mythInterview answer overpromisesPromise at-least-once processing with best-effort dedupe.

Scaling the system

Scale by lane and channel, not only by total worker count. Push, email, SMS, and in-app have different providers, costs, throughput, and failure modes. Transactional and marketing traffic should have separate queues or strict priority so a campaign does not starve password reset.

Partition queues by tenant or user id for fairness, but be careful with ordering. For notifications, strict ordering is usually category-specific rather than global. A password reset should not wait behind a newsletter for the same user. Use priority and scheduled time in the job, then let workers pull within provider budgets.

Campaign fan-out should be batched. A campaign job can expand recipients into routing jobs in chunks, respecting preference snapshots and provider budgets. Do not create a single enormous message that one worker must process for hours. Monitor queue age by lane because CPU can look normal while provider quotas are the real ceiling.

Failure handling

FailureWhat happensWhat to build
Provider 429Sends slow downToken bucket, backoff with jitter, protect transactional lane.
Provider 5xxTransient failuresRetry with capped attempts and failover if configured safely.
Invalid push tokenPush never reaches deviceMark token inactive on terminal provider error.
Hard email bounceFuture email should stopAdd suppression and protect sender reputation.
Template render errorJob cannot be sent correctlyDLQ and alert; fix template or payload, then replay.
Preference service downRouter cannot decideFail closed for marketing; use careful cached preferences for allowed transactional categories only if policy permits.
Queue backlogOTPs arrive lateScale workers if provider budget allows; shed or pause marketing first.

The degraded state is delayed or suppressed noncritical messages. The outage state is lost or severely delayed transactional messages, stale opt-out enforcement, or uncontrolled duplicate sends.

API design

EndpointRole
POST /v1/notificationsAccept a logical notification request and return 202.
GET /v1/notifications/{notification_id}Read status and attempts.
POST /v1/users/{user_id}/preferencesUpdate preferences and quiet hours.
GET /v1/templates/{template_id}Inspect template version metadata.
POST /v1/internal/dlq/{message_id}:replayReplay a fixed dead-letter message.

POST /v1/notifications should include user_id, template_id, template_version if pinned, category, data, optional channels, optional send_after, and dedupe_key or idempotency_key. The response should include notification_id, initial status, and a status URL.

POST /notifications
  -> validate producer and template contract
  -> dedupe by key
  -> save notification
  -> enqueue routing job
  -> 202 Accepted

router -> prefs/template/destinations -> channel jobs -> providers

Important errors: 400 for malformed payload, 404 for missing template, 409 for idempotency conflict with different payload, 429 for producer quota, and 503 when the service cannot durably enqueue the request.

Observability

SignalWhy it matters
Queue age by lane and channelShows OTP starvation before users complain.
Provider error code rateSeparates invalid tokens, throttling, and provider outages.
Dedupe hit rateReveals producer retry storms or duplicate events.
Suppression rate by category and preference versionCatches stale opt-out cache and policy mistakes.
Template render failuresFinds bad deploys quickly.
DLQ depth and oldest message agePrevents poison work from becoming invisible.
Delivery latency p50/p95/p99 by channelShows whether channel promises are realistic.
Marketing shed countProves the system protected transactional traffic during incidents.

A useful dashboard follows one notification from accepted to routed to attempted to terminal status. If all you have is "provider API returned 200," you cannot explain missing OTPs or compliance complaints.

Security and abuse

Authenticate producer services and authorize which templates and categories they may send. A random service should not be able to send password-reset messages or marketing campaigns to every user. Validate template variables so producers cannot inject unsafe HTML or links. Store secrets for provider credentials in a secure vault and rotate them.

Preferences and unsubscribe are security and compliance concerns, not only UX. Marketing opt-out must be enforced before fan-out, logged with preference version, and protected from stale cache. Phone numbers and email addresses are personal data; avoid logging full destinations in ordinary logs. Rate-limit producers so one bug or compromised service cannot send millions of messages.

Cost awareness

SMS is expensive. Email reputation is fragile. Push is cheaper but not guaranteed to be seen. A good design chooses channels by category and user preference instead of blasting every channel for every event. For example, a password reset may use email plus SMS fallback only after delay, while a marketing campaign should obey frequency caps and cheaper channels first.

Retries also cost money. Retrying a permanent template error wastes workers. Retrying provider 429s too aggressively can make throttling worse. DLQ, backoff, and channel budgets are cost controls as much as reliability controls. Lane isolation also protects revenue: spending provider quota on marketing while OTPs fail is both costly and visible.

Production scenes

Scene 1: Password reset waits behind marketing

The moment is familiar: a campaign launches, queue depth rises, and password reset codes arrive minutes late. CPU may be fine because the bottleneck is provider rate limits or a shared queue, not compute.

The trap is one queue for everything. Workers process a giant marketing batch while transactional jobs sit behind it. What to build: separate transactional and marketing lanes, queue-age alerts by lane, and a shed rule that pauses marketing when OTP latency crosses budget.

Scene 2: Opted-out users still get marketing

A user unsubscribes, then receives another promotional SMS. The provider may have delivered exactly what the system asked it to deliver. The mistake happened earlier: stale preferences or fan-out before policy.

What to build: preference checks before channel jobs, invalidation on preference update, preference version recorded on every send, and fail-closed behavior for marketing when preferences are unavailable.

Scene 3: Provider failover doubles the message

The primary SMS provider times out. The worker sends the same OTP through a backup provider. Later, the primary provider delivers the original message too. The user sees two codes and loses trust.

Provider failover must preserve logical dedupe and understand provider uncertainty. If the primary result is unknown, ask the provider status API when possible, use a stable provider reference, and decide whether the product prefers delay or possible duplicate. Do not blindly send through every provider on timeout.

Scene 4: A bad template poisons the queue

A template deploy removes a required variable. Every job for that template fails rendering. If workers retry forever, the queue fills with work that cannot succeed and hides good messages behind it.

What to build: template validation before rollout, render checks at ingest or routing, DLQ after a small number of terminal render failures, and replay tooling after the template is fixed.

In the interview: Pick one scene and connect it to a mechanism. "Marketing starved OTP" maps to lane isolation. "Opt-out ignored" maps to preference version audit. "Bad template blocked queue" maps to DLQ.

Bottlenecks and tradeoffs

Throughput vs provider quotas

You can scale workers faster than providers accept traffic. If you ignore quotas, you create 429 storms and retry cost. Use per-provider token buckets and scale based on allowed send rate, not only queue depth.

Transactional speed vs marketing reach

Marketing wants large fan-out. Transactional wants low latency. Shared queues make this a fight. Separate lanes make the tradeoff explicit and let you shed marketing first.

Preference freshness vs router latency

Preference reads can be hot. Caching helps, but stale opt-out creates compliance incidents. Use short TTLs, explicit invalidation, and stricter reads for marketing than for harmless product reminders.

Dedupe strength vs user-visible duplicates

Internal dedupe reduces duplicates, but provider and device behavior can still create them. The honest landing point is logical idempotency, attempt tracking, and client or in-app collapse, not a claim of perfect exactly once.

Interview tips

Exactly once is the wrong promise

You might say: "We guarantee exactly-once delivery to every user."

Interview Push: "What if the provider accepted the request but your worker timed out, or the user has two devices?"

Land here: Promise durable enqueue and at-least-once processing with dedupe for the logical notification. Store a dedupe key, record attempts, and collapse duplicates where you control the client or inbox. Be honest that provider networks and human attention are outside your exact control.

202 does not mean delivered

You might say: "The send API returns success after it calls FCM."

Interview Push: "Should checkout wait for FCM, APNs, Twilio, and email?"

Land here: No. POST /notifications returns 202 after validation, dedupe, save, and enqueue. Provider delivery happens later. The status API can show attempts and terminal states, but the producer should not block its user action on external notification providers.

Preferences must run before fan-out

You might say: "We create channel jobs first, then each worker checks opt-out."

Interview Push: "A user opts out of marketing SMS. Why did a job still reach the SMS queue?"

Land here: Router workers should check preferences, quiet hours, category, and destination suppressions before channel jobs are created. Store the preference version used for the decision. Marketing should fail closed if preferences are unavailable, because stale opt-out is more serious than delayed promotion.

Rate limits need budgets, not just retries

You might say: "If Twilio returns 429, we retry until it works."

Interview Push: "What stops all workers from retrying at once and making the throttle worse?"

Land here: Use per-provider token buckets, backoff with jitter, and circuit breakers. Slow or pause the provider lane when it is throttled. Protect transactional messages first, and shed marketing if the backlog threatens important messages.

DLQ is for poison, not laziness

You might say: "Failed messages go to a dead-letter queue."

Interview Push: "Which failures belong there, and what happens next?"

Land here: DLQ is for work that will not succeed by simple retry: invalid template variables, unsupported locale, malformed payload, or repeated terminal provider errors. The DLQ needs alerts, ownership, and replay after a fix. It is not a place to hide messages forever.

What should stick

  1. Ingest is not delivery. 202 means saved and queued, not provider delivered or user read.
  2. Route before fan-out. Preferences, category, quiet hours, template, and destinations decide which channel jobs exist.
  3. Separate lanes. Transactional traffic must not wait behind marketing blasts.
  4. Dedupe beats exactly-once claims. Use logical keys, attempt records, and client or inbox collapse.
  5. Providers shape the system. Rate limits, bounces, invalid tokens, and failover rules are part of the design.

Tell it in the room: "Producers call POST with a dedupe key. We save and enqueue, return 202, route through preferences and templates, create channel jobs, and send through provider workers with rate limits and retries. Transactional and marketing lanes are separate. Poison jobs go to DLQ, and exactly-once to a human is not the promise."

Key Takeaways

  • Separate ingest, routing, and delivery so producers do not wait on providers.
  • Check preferences and quiet hours before creating channel jobs.
  • Use template versions so retries do not change message content unexpectedly.
  • Protect transactional lanes from marketing campaigns.
  • Retry transient provider errors with backoff and jitter; stop retrying poison payloads.
  • Track delivery attempts and provider error codes, not only API success.
  • Explain exactly-once as a myth and offer a realistic dedupe story.

Quick revision notes

Clarify categories and channels. Ask whether the system handles transactional, marketing, or both; which channels exist; and what preferences apply.

Draw three phases. Ingest saves and queues. Router applies policy and creates channel jobs. Channel workers send with retries and rate limits.

Protect important traffic. OTPs, receipts, and security alerts need a separate lane or strict priority so campaigns cannot starve them.

Do not promise exactly once. Promise idempotent logical notifications, attempt tracking, and dedupe where you control the system.

Operate provider reality. Watch queue age, provider 429, invalid tokens, bounce rates, DLQ depth, and preference-version usage.

Practice next: Open Notification System Design on Practice System Design and run a timed attempt using one OTP and one marketing blast to explain lane isolation.

What interviewers expect

A strong answer separates durable enqueue from async delivery, uses lane isolation for transactional and marketing traffic, checks preferences before fan-out, renders versioned templates, dedupes logical notifications, retries transient provider failures, sends poison messages to DLQ, and measures queue age by channel and priority.

Interview workflow (template)

  1. Clarify requirements. Confirm functional scope, users, consistency needs, and which non-functional goals matter most (latency, availability, cost).
  2. Rough capacity. Estimate QPS, storage, and bandwidth so your data model and partitioning story are grounded.
  3. APIs and core flows. Define a minimal API and walk 1–2 critical read/write paths end to end.
  4. Data model and storage. Choose stores for each access pattern; call out hot keys, indexes, and retention.
  5. Scale and failure. Add caching, sharding, replication, queues, or fan-out as needed; say what breaks in failure modes.
  6. Tradeoffs. Name alternatives you rejected and why (e.g. strong vs eventual consistency, sync vs async).

Frequently asked follow-ups

  • How do email, SMS, and push fan-out work?
  • How do you prevent duplicate notifications during retries?
  • How do preferences and quiet hours apply?
  • How do provider rate limits and failover work?
  • Why is exactly-once notification delivery not a real promise?

Deep-dive questions and strong answer outlines

What does POST notification return?

It should return 202 Accepted after the request is validated, deduped, persisted, and queued. It should not wait for APNs, FCM, Twilio, or an email provider to deliver the message.

How do you route to multiple channels?

A router loads preferences, template metadata, priority, and available destinations. It creates channel jobs for push, email, SMS, or in-app only when allowed, then channel workers send through provider adapters.

How do you avoid duplicate sends?

Use a producer-provided idempotency key or dedupe key such as event id plus user id plus notification type. Store a send record and make retries reuse the same logical notification. Some providers may still deliver twice, so client or inbox dedupe also helps.

How do you handle provider limits?

Use per-provider and per-tenant token buckets, separate queues by channel and priority, exponential backoff with jitter on 429 and 5xx, and provider failover only when templates, numbers, domains, and compliance allow it.

What goes to the DLQ?

Poison messages such as invalid template variables, unsupported locale, malformed payload, or repeated terminal provider errors should go to a dead-letter queue with alerts and replay tooling. Retrying them forever blocks useful work.

AI feedback on your design

After a practice session, InterviewCrafted summarizes strengths, gaps, and interviewer-style expectations—similar to a written debrief. See a static example report, then practice this problem to get feedback on your own answer.

FAQs

Q: Are we building APNs, FCM, Twilio, or SendGrid?

A: No. The system integrates with providers through adapters. The design work is orchestration: durable enqueue, routing, templates, preferences, retries, rate limits, failover, and delivery tracking.

Q: Can we guarantee exactly-once delivery?

A: Not end to end. Networks timeout, providers retry, phones uninstall apps, and users may have multiple devices. Promise at-least-once processing with dedupe for the logical notification and honest status tracking.

Q: Should marketing and transactional messages share one queue?

A: They should not share one unbounded lane. Password resets, receipts, and security alerts need their own priority or queue so a marketing campaign cannot delay them.

Q: Where are user preferences checked?

A: Check preferences before channel fan-out. For marketing, opt-out and quiet hours must be enforced carefully and audited. Transactional messages may bypass some marketing preferences, but that rule should be explicit.

Practice interactively

Open the practice session to use the canvas and stages, then review AI feedback.