Design a Notification Service
Modern software systems rely on notifications to engage users, deliver critical alerts, and drive business outcomes. Notifications appear as order confirmations, password reset links, payment receipts, security alerts, product recommendations, and team mentions. Each business domain—e-commerce, banking, SaaS, social networks, collaboration platforms, and AI applications—depends on timely, reliable notification delivery.
Despite this universal need, many engineering teams duplicate notification logic inside individual services. Each service implements its own email client, SMS integration, and push delivery mechanism. This approach creates technical debt, increases maintenance cost, and produces inconsistent user experiences.
A Notification Service solves this problem by providing a shared platform capability for notification delivery. It decouples business applications from channel-specific delivery logic and external notification providers. Business services simply call a well-defined API and let the notification service handle routing, formatting, delivery, retries, and provider failover.
This article teaches you how to design a production-grade notification service that supports multiple channels—email, SMS, push, in-app, and webhooks—while meeting strict reliability, scalability, and observability requirements. We will focus on service boundaries, asynchronous processing, provider abstraction, template management, user preferences, and distributed system trade-offs.
Step 1: Requirements Clarification
Before designing the architecture, we must define what the notification service must do and how well it must perform.
Functional Requirements
- Send notification – Deliver a message to one or more recipients via one or more channels.
- Support multiple channels – Email, SMS, push notifications, in-app notifications, and webhooks.
- Support templates – Use parameterized templates for consistent formatting and localization.
- User preference management – Respect user opt-in/opt-out choices, channel preferences, and quiet hours.
- Notification priority – Support high-priority (urgent) and low-priority (bulk) notifications.
- Scheduled delivery – Deliver notifications at a future time or on a recurring schedule.
- Retry failed deliveries – Automatically retry transient failures with exponential backoff.
- Delivery status tracking – Provide visibility into whether a notification was sent, delivered, or failed.
- Notification history – Store complete delivery records for audit and debugging.
- Provider failover – Automatically switch to a secondary provider when the primary provider fails.
- Localization – Deliver notifications in the recipient's preferred language.
- Tenant support – Support multi-tenancy for SaaS platforms with isolated configuration.
Non-Functional Requirements
- High availability – The service must remain available even when external providers experience outages.
- High throughput – Handle millions of notifications per day with consistent performance.
- Low latency for urgent notifications – Critical notifications must be delivered within seconds.
- Reliable delivery – Achieve at-least-once delivery semantics where possible.
- Horizontal scalability – Scale API servers and workers independently based on load.
- Fault tolerance – Continue operating through provider failures, queue backlogs, and database issues.
- Idempotency – Duplicate notification requests must not result in duplicate deliveries.
- Observability – Expose metrics, logs, and traces to monitor service health and delivery performance.
- Security – Authenticate callers, authorize actions, protect PII, and prevent abuse.
Step 2: Capacity Estimation
Capacity estimation helps us size the infrastructure and identify bottlenecks.
Consider a large SaaS platform with:
- 10 million active users
- 50 million notifications generated per day (average)
- Peak burst – 2x average during flash sales or security events
- Notifications per second – 50 million / 86,400 ≈ 580 NPS average; peak ~1,200 NPS
- Channel distribution – Email 50%, SMS 20%, Push 25%, In-App 5%
- Retry volume – 5–10% of initial deliveries fail transiently and require retries
- Average payload size – 5 KB (including metadata)
- Storage per day – 50M × 5 KB = 250 GB/day
- Monthly storage – ~7.5 TB
- Annual storage – ~90 TB with retention policies
Key insight: Notification services are highly bursty. Traffic spikes occur during marketing campaigns, product launches, security incidents, and system maintenance events. The architecture must handle bursts gracefully without dropping messages.
Step 3: API Design
The notification service exposes a RESTful API for internal clients. All endpoints require authentication and authorization.
Send Notification
POST /api/v1/notifications
Request body:
{
"idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
"tenant": "acme-corp",
"recipients": [
{
"user_id": "user-123",
"phone": "+1234567890",
"device_tokens": ["token-abc", "token-def"]
}
],
"channels": ["email", "sms", "push"],
"priority": "high",
"template_id": "order-confirmation",
"template_params": {
"order_id": "ORD-2026-001",
"total": "$49.99",
"delivery_date": "2026-09-10"
},
"locale": "en-US",
"schedule": {
"delivery_time": "2026-09-06T10:00:00Z",
"timezone": "America/New_York"
},
"metadata": {
"source_system": "order-service",
"correlation_id": "req-456"
}
}
Response:
{
"notification_id": "notif-789",
"status": "accepted",
"accepted_at": "2026-09-05T14:30:00Z"
}
Schedule Notification
POST /api/v1/notifications/schedule
Same as send but requires a future delivery_time.
Cancel Scheduled Notification
DELETE /api/v1/notifications/{notification_id}
Get Notification Status
GET /api/v1/notifications/{notification_id}
Get Notification History
GET /api/v1/notifications?user_id=user-123&limit=50
Update User Preferences
PUT /api/v1/users/{user_id}/preferences
{
"channels": {
"email": true,
"sms": false,
"push": true
},
"categories": {
"marketing": false,
"security": true,
"order_updates": true
},
"quiet_hours": {
"start": "22:00",
"end": "08:00",
"timezone": "America/New_York"
},
"locale": "es-MX"
}
Register Notification Template
POST /api/v1/templates
{
"template_id": "password-reset",
"tenant": "acme-corp",
"channels": {
"email": {
"subject": "Reset your password",
"body_html": "<h1>Hello {{name}}</h1><p>Reset link: {{reset_link}}</p>",
"body_text": "Hello {{name}}. Reset link: {{reset_link}}"
},
"sms": {
"body": "Reset link: {{reset_link}}"
}
},
"locale": "en-US"
}
Register Delivery Provider
POST /api/v1/providers
{
"channel": "email",
"provider_name": "sendgrid",
"priority": 1,
"config": {
"api_key": "encrypted-key",
"endpoint": "https://api.sendgrid.com/v3/mail/send"
},
"regions": ["us-east", "us-west"]
}
Idempotency: The idempotency_key ensures that duplicate requests from business services do not result in duplicate notifications. The service stores the key for 7 days and returns the same notification_id for identical keys.
Step 4: High-Level Architecture
The notification service follows an event-driven, asynchronous architecture. Business services call the Notification API, which validates requests, applies idempotency, stores the notification record, and publishes a message to a queue. Worker processes consume messages, render templates, evaluate user preferences, route to the appropriate channel, and deliver via provider adapters.
┌─────────────────────────────────────────────────────────────────────┐
│ Business Services │
│ (Order Service, Auth Service, Payment Service, CRM, etc.) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (Authentication, Rate Limiting, Routing) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Notification API │
│ (Validation, Idempotency, Orchestration) │
└─────────────────────────────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ Template Service │ │ Preference Service │
│ (Render & Localize) │ │ (User Preferences / Opt) │
└─────────────────────────┘ └─────────────────────────────┘
│ │
└──────────────┬───────────────┘
▼
┌─────────────────────────────┐
│ Notification Store │
│ (Metadata, Status, History)│
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Message Queue │
│ (Priority / Partitioned) │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Notification Workers │
│ (Channel-Specific Pools) │
└─────────────────────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ Channel Router │ │ Delivery Status Processor │
│ (Provider Selection) │ │ (Status Updates / Callbacks)│
└─────────────────────────┘ └─────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Provider Adapters │
├─────────────┬─────────────┬─────────────┬─────────────────┤
│ Email │ SMS │ Push │ Webhook │
│ Adapter │ Adapter │ Adapter │ Adapter │
└─────────────┴─────────────┴─────────────┴─────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ External Providers │
│ SendGrid, Twilio, FCM, APNS, AWS SNS, etc. │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Retry Queue / DLQ │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Monitoring Platform │
│ (Metrics, Logs, Alerts) │
└─────────────────────────────┘
Step 5: Notification Request Flow
The complete flow for a single notification request illustrates the asynchronous nature of the service.
- Business Service – An order service generates an order confirmation event.
- Notification API – Receives the request, validates the payload, and checks the idempotency key.
- Validation and Idempotency – If the key already exists, return the previous
notification_id. Otherwise, proceed. - Notification Store – Write the notification record with status "pending".
- Message Queue – Publish a message containing the notification data to the appropriate queue.
- API Response – Return
notification_idand status "accepted" to the client immediately. - Channel Worker – Consume the message, evaluate user preferences, and determine the channel.
- Template Rendering – Render the template with the provided parameters and locale.
- Provider Selection – Choose the appropriate provider based on health, cost, and region.
- External Provider – Call the provider API with the rendered content.
- Delivery Status – Update the notification store with the final status (delivered, failed, etc.).
- Retry on Failure – If the delivery fails transiently, publish to the retry queue with exponential backoff.
- Dead Letter – If retries are exhausted, move the message to the dead letter queue for manual inspection.
Why asynchronous? Business requests should not wait synchronously for external provider delivery. Provider APIs can be slow, unreliable, or temporarily unavailable. Synchronous delivery would increase latency, consume thread pools, and create cascading failures. Asynchronous delivery decouples the business application from provider availability and allows the service to gracefully handle provider degradation.
Step 6: Notification Orchestration
Orchestration determines how the service processes a notification request.
Channel Selection: The service evaluates the request's channels field, the user's channel preferences, and the notification category. For example, a security alert may force-send via SMS even if the user has SMS disabled in preferences.
Template Selection: The service uses the provided template_id and locale to render the appropriate template version. If a template is not available for the requested locale, it falls back to the default locale.
Priority: High-priority notifications bypass low-priority queues and receive faster processing. The system reserves capacity for urgent notifications such as password resets and security alerts.
User Preferences: The service checks opt-in/opt-out status, channel preferences, quiet hours, and frequency limits before delivery. If a user has opted out of marketing emails, the service will not send marketing notifications via email.
Provider Selection: The service selects a provider based on provider health, cost, region, and reliability. It can route traffic away from unhealthy providers and towards cost-effective options.
Synchronous vs Asynchronous Orchestration: The API layer performs minimal orchestration (validation, idempotency, storage) synchronously. The heavy orchestration (template rendering, preference evaluation, provider routing, delivery) happens asynchronously in the worker layer. This separation keeps API response times low while allowing complex processing.
Step 7: Multi-Channel Architecture
The notification service supports multiple channels through a provider-adapter abstraction.
Email
Email is the most common notification channel. The service integrates with providers like SendGrid, Amazon SES, Mailgun, and SMTP servers. Email templates include HTML and plain-text versions, subject lines, attachments, and headers.
SMS
SMS provides high reach but is more expensive and limited to 160 characters per message. The service integrates with providers like Twilio, Vonage, and AWS SNS. SMS messages are concise and often include links or shortcodes.
Push Notifications
Push notifications deliver real-time alerts to mobile devices via FCM (Firebase Cloud Messaging) for Android and APNS (Apple Push Notification Service) for iOS. Push notifications include a title, body, sound, badge count, and deep-link data.
In-App Notifications
In-app notifications appear within the application user interface—as toast messages, banners, or notification centers. These are delivered via WebSocket connections or persisted in a notification store and polled by the client.
Webhooks
Webhooks allow external systems to receive notification delivery events. When a notification is delivered, the service sends an HTTP POST request to a configured webhook URL with delivery details.
| Channel | Latency | Cost | Reliability | User Reach | Provider Dependency | Typical Use Cases |
|---|---|---|---|---|---|---|
| High (1-10s) | Low | Medium | All users | High | Order confirmations, newsletters, receipts | |
| SMS | Medium (1-5s) | Medium | High | Mobile users | High | 2FA, security alerts, delivery updates |
| Push | Low (< 1s) | Low | Medium | App users | High | Chat messages, real-time alerts |
| In-App | Very Low (< 100ms) | Low | High | App users | None | UI notifications, message centers |
| Webhooks | Variable | Free | Medium | Integrations | None | Delivery callbacks, event streaming |
Provider-Adapter Abstraction
Each channel implements a provider adapter interface with standard methods:
send(notification, recipient)– Send the notification.health()– Check provider health.status(message_id)– Get delivery status.
This abstraction allows the service to add new providers without changing business logic. The interface isolates provider-specific details, making the system maintainable and testable.
Step 8: Template Management
Templates decouple notification content from business logic. Business services provide structured data; the notification service renders the final message.
Template Storage: Templates are stored in a database or blob storage with support for versioning. Each template has a unique ID, tenant isolation, and channel-specific content.
Template Versions: Changes to templates create new versions. The service can preview templates and rollback to previous versions if issues arise.
Variables: Templates use placeholder variables like {{order_id}} and {{name}}. The service applies the provided template_params to render the final message.
Localization: Templates support multiple locales. The service selects the correct locale based on user preferences or the request.
Rendering: The service renders templates using a secure templating engine (e.g., Mustache, Handlebars) that prevents injection attacks.
Preview: Business teams can preview templates before deployment using mock data.
Approval: Template changes may require approval in regulated industries.
Rollback: If a template change causes issues, the service can rollback to a previous version.
Why not construct content directly? Business services should not construct notification content directly because it leads to duplication, inconsistency, and difficulties in localization. A centralized template service ensures consistent branding, simplifies localization, and allows non-engineers to manage content.
Step 9: User Preferences
User preferences determine whether and how a user receives notifications. The preference service stores and evaluates these preferences.
Channel Preferences: Users can enable or disable specific channels (email, SMS, push, in-app).
Notification Categories: Users can opt-in or opt-out of categories (marketing, security, order updates, product recommendations).
Opt-in / Opt-out: The service respects opt-out requests globally or per category. Opt-out applies to future notifications, not historical ones.
Quiet Hours: Users can define time windows when they do not want to receive non-urgent notifications. The service holds notifications until quiet hours end.
Frequency Limits: The service limits the number of notifications per user per hour or per day to prevent spam.
Language Preferences: Users can select their preferred language for notifications.
Priority Overrides: Users can override priority settings for critical notifications.
Evaluation Flow: Before delivering a notification, the service evaluates preferences in the following order:
- Global opt-out – block all notifications.
- Category opt-out – block specific category.
- Channel preference – block specific channel.
- Quiet hours – delay if not urgent.
- Frequency limits – delay or drop if rate exceeded.
Step 10: Queue and Asynchronous Processing
Message queues are the backbone of the notification service. They decouple the API layer from the worker layer, provide buffering during traffic bursts, and enable retry and dead-letter handling.
Message Queues: The service uses a distributed message broker such as Kafka, RabbitMQ, or SQS. Each channel has its own queue or partition to isolate workloads.
Partitioning: Large queues are partitioned by tenant, channel, or region to improve throughput and reduce contention.
Consumer Groups: Workers within a consumer group process messages in parallel. The group ensures each message is processed exactly once per group.
Backpressure: If workers are overwhelmed, the queue provides backpressure by slowing message consumption. This protects the system from overload.
Queue Prioritization: High-priority queues receive preferential processing. Low-priority messages may wait longer.
Delayed Messages: Scheduled notifications are published with a delay. The broker holds the message until the delivery time.
Retry Queues: Failed messages are moved to a retry queue with an exponential backoff delay. The service retries up to a maximum number of attempts.
Dead Letter Queues: Messages that fail all retry attempts are moved to a dead letter queue for manual inspection and reprocessing.
Step 11: Retry and Delivery Guarantees
External providers are unreliable. The notification service implements robust retry strategies to handle transient failures.
At-most-once delivery: The service makes a single attempt. This is appropriate for non-critical notifications.
At-least-once delivery: The service retries failed deliveries until success or maximum attempts. This is the default strategy for most notifications.
Practical exactly-once: True exactly-once is difficult with external providers. The service uses idempotency keys to prevent duplicate deliveries when possible.
Exponential backoff: Retries use exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s, 32s, 60s. Jitter prevents thundering herd problems.
Maximum retry attempts: The service limits retries to 5–10 attempts depending on priority.
Permanent vs transient failures: The service distinguishes between permanent failures (e.g., invalid email address) and transient failures (e.g., provider timeout). Permanent failures are sent to the DLQ immediately; transient failures are retried.
Provider failover: If a provider continuously fails, the service routes traffic to a secondary provider.
Idempotent delivery: The service uses idempotency keys to ensure duplicate delivery requests produce only one actual delivery. The provider may also support idempotency (e.g., SendGrid's unique_args).
Why not a database transaction? Delivery to external providers cannot be treated as a simple database transaction. Provider APIs are inherently distributed and eventually consistent. The notification service uses queues, retries, and status tracking to achieve reliable delivery without distributed transactions.
Step 12: Provider Abstraction and Failover
Provider abstraction allows the service to support multiple providers per channel and automatically failover between them.
Provider Registration: Providers are registered in the service configuration with priority, regions, capacity, and cost.
Provider Health: The service periodically checks provider health via health endpoints or delivery success rates. Unhealthy providers are marked as degraded.
Cost-based routing: The service routes traffic to the lowest-cost provider with sufficient capacity.
Region-based routing: The service routes traffic to the provider closest to the recipient's region.
Reliability-based routing: The service routes traffic to the provider with the highest delivery success rate.
Automatic failover: If a provider becomes unhealthy or starts failing, the service automatically reroutes traffic to the next available provider.
Circuit Breakers: The service uses circuit breakers to prevent repeated calls to failing providers. After a configurable failure threshold, the circuit opens and the provider is bypassed.
Step 13: Notification Storage
The notification store persists notification metadata, status, history, templates, preferences, and provider configurations.
Data Models
Notification
CREATE TABLE notifications (
notification_id UUID PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
idempotency_key VARCHAR(128) UNIQUE,
template_id VARCHAR(64),
locale VARCHAR(10),
priority VARCHAR(20),
status VARCHAR(20), -- pending, queued, delivered, failed, retrying, dead_letter
created_at TIMESTAMP,
delivered_at TIMESTAMP,
retry_count INT DEFAULT 0,
provider VARCHAR(64),
channel VARCHAR(20),
metadata JSONB,
payload_size INT
) PARTITION BY RANGE (created_at);
Recipient
CREATE TABLE recipients (
recipient_id UUID PRIMARY KEY,
notification_id UUID REFERENCES notifications(notification_id),
user_id VARCHAR(64) NOT NULL,
email VARCHAR(255),
phone VARCHAR(20),
device_tokens JSONB,
status VARCHAR(20),
error_code VARCHAR(64),
error_message TEXT
);
Delivery Attempt
CREATE TABLE delivery_attempts (
attempt_id UUID PRIMARY KEY,
notification_id UUID REFERENCES notifications(notification_id),
attempt_number INT,
provider VARCHAR(64),
status VARCHAR(20),
latency_ms INT,
error_code VARCHAR(64),
attempted_at TIMESTAMP
);
Template
CREATE TABLE templates (
template_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
name VARCHAR(128),
channel VARCHAR(20),
locale VARCHAR(10),
version INT,
content JSONB, -- subject, body_html, body_text
is_active BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
User Preference
CREATE TABLE user_preferences (
user_id VARCHAR(64) PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
channels JSONB, -- email, sms, push, in-app
categories JSONB, -- marketing, security, updates
quiet_hours JSONB, -- start, end, timezone
locale VARCHAR(10),
opt_out BOOLEAN DEFAULT FALSE,
updated_at TIMESTAMP
);
SQL vs NoSQL
The notification service benefits from a relational database for transactional consistency, but may use NoSQL for high-volume history storage. A hybrid approach works well: SQL for transactional data (notifications, templates, preferences) and NoSQL (e.g., Elasticsearch, Cassandra) for history and analytics.
Partitioning: The notifications table is partitioned by created_at to improve query performance and simplify archiving.
Retention: Notifications older than 90 days are archived to cold storage (e.g., S3) and removed from the main database.
Indexing: Indexes on user_id, status, created_at, and tenant_id support common queries.
Step 14: Scalability Strategies
The notification service must scale horizontally to handle increasing load.
Stateless API servers: API servers are stateless and can be scaled horizontally behind a load balancer.
Horizontal worker scaling: Workers scale independently of API servers. More workers increase throughput.
Queue partitioning: Large queues are partitioned by tenant, channel, or region. Each partition has its own workers.
Channel-specific worker pools: Email, SMS, push, and in-app channels have independent worker pools. This prevents one channel from starving others.
Tenant isolation: High-volume tenants can have dedicated workers and queues to prevent noisy neighbor issues.
Database sharding: The notification database is sharded by tenant or region to distribute write and query load.
Multi-region deployment: The service can be deployed in multiple regions for low latency and disaster recovery.
Autoscaling: Workers can be autoscaled based on queue depth. When the queue grows, more workers are added; when it shrinks, workers are removed.
Independent scaling: Different channels may require independent scaling. Email workers may need more capacity than SMS workers because email has higher volume.
Step 15: Reliability and Fault Tolerance
The notification service remains available through failures of components and external providers.
Retry: Transient failures are retried with exponential backoff.
Circuit Breaker: Circuit breakers prevent repeated calls to failing providers.
Timeout: Timeouts limit how long the service waits for provider responses. If a timeout occurs, the delivery is marked as failed and retried.
Bulkhead: Channel-specific worker pools isolate failures. A flood of email traffic does not affect SMS delivery.
Queue buffering: Queues provide buffering during traffic bursts. If workers become slow, messages queue safely.
Provider failover: Traffic is rerouted to healthy providers automatically.
Graceful degradation: If a provider is unavailable, the service degrades gracefully. It retries later or falls back to another channel.
Failure Scenarios
Email provider fails: The service marks the provider as degraded and routes to the secondary provider. If both fail, messages go to the retry queue.
SMS provider becomes slow: The service times out slow requests and retries. Circuit breakers open if the provider becomes consistently slow.
Push provider unavailable: The service caches messages and retries when the provider recovers.
Database unavailable: The service uses read replicas for queries and buffers writes in the queue. If the database is down, new notifications are still accepted but status updates are delayed.
Queue backlog grows rapidly: Workers autoscale to process the backlog. New API requests may be rate-limited to prevent further growth.
Step 16: Scheduling and Delayed Delivery
Scheduled notifications are delivered at a future time.
Scheduled notifications: The notification API accepts a delivery_time in the request. The service stores the scheduled time and publishes the message to a scheduled queue.
Delayed queues: The message broker supports delayed delivery. Messages are not visible to consumers until the scheduled time.
Time zones: The service converts the delivery time to UTC and respects the recipient's timezone for quiet hours.
Recurring notifications: The service supports recurring notifications (e.g., daily, weekly) by generating a new notification event at each interval.
Cancellation: Scheduled notifications can be cancelled before delivery. Cancellation removes the message from the queue.
Expiration: Scheduled notifications have a maximum delay. If the scheduled time is too far in the future, the service may reject the request.
Why separate scheduling? Scheduling should be separated from immediate delivery to prevent long delays from blocking immediate messages. The scheduling service publishes to the queue at the correct time, maintaining separation of concerns.
Step 17: Observability
Observability is critical for operating a notification service at scale.
Metrics
- Delivery success rate – Percentage of successfully delivered notifications per channel and provider.
- Delivery latency – P95 and P99 latency from API acceptance to successful delivery.
- Queue depth – Number of messages waiting in each queue.
- Retry count – Number of retry attempts per notification.
- Provider error rate – Error rate per external provider.
- Notification throughput – Notifications processed per second.
- Template rendering failures – Failed template renders due to invalid data.
- Dead letter queue size – Number of messages that exhausted retries.
Logs
Structured logs capture each delivery attempt, provider response, error, and correlation ID. Logs are aggregated and searchable.
Distributed Tracing
Traces follow a notification from API request through queue processing to provider delivery and status update. Tracing helps identify bottlenecks and diagnose failures.
Correlation IDs
Each notification request receives a correlation ID. This ID is passed to all downstream systems and included in logs to trace the entire flow.
Alerts
Alerting rules notify engineers when success rates drop, latency spikes, queue depths grow, or DLQ size exceeds thresholds.
SLOs
Service Level Objectives define acceptable performance:
- Availability: 99.95% uptime
- Delivery success rate: > 99.5%
- P95 delivery latency: < 30 seconds for urgent notifications
- Queue backlog: < 1 hour of average traffic
Step 18: Security and Abuse Prevention
The notification service handles sensitive user data and must protect against abuse.
Authentication: All API requests require authentication via API keys or JWT tokens. The service verifies the caller's identity.
Authorization: The service enforces tenant isolation. Callers can only access notifications for their tenant.
Rate limiting: The service rate-limits callers to prevent abuse. Limits are per tenant, per second, and per day.
Tenant isolation: Data is isolated by tenant. One tenant cannot access another tenant's data.
PII protection: Personal data (email, phone, device tokens) is encrypted at rest. Logs mask PII.
Encryption: Data is encrypted in transit (TLS) and at rest (AES-256).
Abuse prevention: The service monitors for signs of abuse—high volume, unusual spikes, and suspicious patterns. Abuse reports are flagged for review.
Spam prevention: The service implements domain validation, SPF/DKIM for email, and content filtering to prevent spam.
Audit logging: All actions are audit-logged for compliance and investigation.
Real-World Example: SaaS Platform Notification Service
Consider a large SaaS platform with multiple business systems: Order Service, Auth Service, Payment Service, and CRM. Each system generates events that require notifications.
Business Events
- User registered – Auth Service
- Password reset requested – Auth Service
- Payment completed – Payment Service
- Invoice generated – Payment Service
- Order shipped – Order Service
- Security alert triggered – Security Service
Notification Flow
- Order Service processes a new order and emits an "order_shipped" event to the event bus.
- Event Consumer captures the event and calls the Notification API.
- Notification API validates the request, applies idempotency, and stores the notification.
- Orchestration:
- Determines the channel: email and SMS (high priority for shipping).
- Evaluates user preferences: user has opted out of SMS but opted in to email.
- Renders the template: "Your order #12345 has shipped" with tracking link.
- Selects the provider: SendGrid (primary), SES (secondary).
- Queue: Message is published to the email queue.
- Worker: Consumes the message, calls SendGrid.
- SendGrid delivers the email to the user.
- Status Update: The notification store marks the notification as "delivered".
- Callback: The service sends a webhook to the Order Service with delivery confirmation.
Trade-offs
Synchronous vs asynchronous delivery: Synchronous delivery provides immediate feedback but increases latency and couples business services to provider availability. Asynchronous delivery decouples systems and improves resilience.
Single provider vs multi-provider: Multi-provider increases reliability and cost flexibility but adds complexity. Single provider is simpler but creates vendor lock-in and single points of failure.
At-least-once vs stronger delivery guarantees: At-least-once is practical and simple but may cause duplicate deliveries. Stronger guarantees are difficult with external providers and require complex coordination.
Centralized notification service vs service-specific logic: Centralization reduces duplication and ensures consistency but creates a single point of failure and dependency. Service-specific logic is simpler per service but duplicates effort and becomes unmanageable.
Immediate delivery vs scheduled delivery: Immediate delivery is simpler and faster but lacks flexibility. Scheduled delivery adds complexity but supports time-sensitive campaigns and quiet hours.
Cost vs reliability: Higher reliability costs more—multiple providers, redundant infrastructure, and larger queues. The service must balance cost against business requirements.
Simplicity vs provider redundancy: Redundancy adds complexity in routing, health checks, and configuration. Simplicity reduces operational overhead but risks provider outages.
Common Mistakes
- Sending notifications synchronously from business requests – This blocks business operations and creates cascading failures.
- Tight coupling to one provider – Provider outages cause service-wide failures. Multi-provider failover is essential.
- No idempotency – Duplicate API requests cause duplicate deliveries and angry users.
- No retry strategy – Transient provider failures lead to permanent failures.
- No dead-letter handling – Failed messages are lost without manual inspection.
- Ignoring user preferences – Users opt-out for a reason. Ignoring preferences leads to spam and complaints.
- No provider failover – A single provider outage takes down the entire channel.
- Mixing notification templates with business logic – Business logic should not construct notification content.
- Storing large notification payloads unnecessarily – Large payloads increase storage costs and reduce performance.
- Using one worker pool for every channel – One channel can starve others. Channel-specific pools isolate workloads.
Interview Perspective
System design interviews often ask about notification services. Interviewers evaluate your ability to design a scalable, reliable distributed system.
Typical questions:
- Design a notification service.
- Why use asynchronous messaging?
- How do you support email, SMS, and push?
- How do you handle provider failures?
- How do you guarantee idempotency?
- How do you implement retries?
- How do you scale workers?
- How do you handle scheduled notifications?
- How do you track delivery status?
What interviewers expect:
- Understanding of asynchronous messaging and queues.
- Ability to design a provider-adapter abstraction.
- Knowledge of retry strategies and dead-letter handling.
- Awareness of user preferences and template management.
- Capability to discuss failure scenarios and trade-offs.
Summary
This article presented a comprehensive design for a production-grade notification service. We covered:
- Requirements – Functional and non-functional requirements for a reusable platform service.
- Architecture – API gateway, notification API, template service, preference service, message queue, workers, provider adapters, and storage.
- Notification orchestration – Channel selection, template rendering, preference evaluation, and provider routing.
- Channel abstraction – Support for email, SMS, push, in-app, and webhooks via provider adapters.
- Templates – Centralized management with versioning, localization, and preview.
- Preferences – User opt-in/opt-out, channel preferences, quiet hours, and frequency limits.
- Queues – Asynchronous processing, partitioning, prioritization, and retry/DLQ handling.
- Retries – Exponential backoff, provider failover, and idempotency.
- Provider failover – Health checks, circuit breakers, and automatic routing.
- Storage – Data models, partitioning, retention, and indexing.
- Scalability – Horizontal scaling, queue partitioning, tenant isolation, and autoscaling.
- Reliability – Retry, circuit breakers, timeouts, bulkhead, and graceful degradation.
- Observability – Metrics, logs, tracing, alerts, and SLOs.
- Security – Authentication, authorization, rate limiting, encryption, and audit logging.
A notification service should be treated as a reusable platform capability. It decouples business applications from channel-specific delivery logic and external notification providers. By following the patterns and principles described in this article, you can build a scalable, reliable, and maintainable notification service that meets the needs of modern distributed systems.
Related System Design Articles
Continue your learning with these related articles:
- Design a Chat System – Real-time messaging architecture.
- Design an Email System – Email delivery and processing.
- Design an SMS System – SMS gateway and delivery.
- Design an API Gateway – Routing, authentication, and rate limiting.
- Design a Distributed Cache – Caching strategies and consistency.
- Design a Message Queue – Distributed queuing systems.
- Design a Rate Limiter – Throttling and abuse prevention.
- Design a Search Engine – Indexing and query processing.
- Design a CDN – Content delivery and edge caching.
- Event-Driven Architecture – Event sourcing and CQRS.
- Circuit Breaker Pattern – Resilience and fault tolerance.
- Saga Pattern – Distributed transactions.
- Scalability Explained – Horizontal and vertical scaling.
- Availability vs Reliability – Understanding uptime guarantees.
- Fault Tolerance Explained – Building resilient systems.