Notification Service System Design Template (Reference Design)
Push, email, SMS and in-app: templates, preferences and opt-out, priority queues, caps, provider failover, retries, dedup
使用方法: Use when centralising notifications. Separate critical, transactional and marketing traffic from the first page and fill the channel mix to see SMS cost. The main pitfall is letting campaigns share queues with one-time codes.
预览
System Design: [Notification service name]
Reference design for a multi-channel notification service that sends push, email, SMS and in-app messages on behalf of product teams. It covers templates and localisation, user preferences and opt-out, priority queues, per-user rate limits, provider failover, retries with backoff, deduplication and delivery tracking. It follows the structure of the System Design Document template and is pre-filled with a worked design to adapt. Values marked Example are assumptions. Consent and marketing rules vary by jurisdiction; this is an engineering format, not legal advice.
Document control
| Field | Value |
|---|---|
| Document ID | [SDD-NOTIF-001] |
| System / feature | [Notification platform] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Related | [Preference centre PRD], [Privacy review], [Provider contracts] |
Approvals
| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |
| Privacy / legal | [@name] | [Decision] | YYYY-MM-DD |
| Marketing operations | [@name] | [Decision] | YYYY-MM-DD |
Revision history
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Dedicated queue and providers for one-time codes |
| 0.3 | YYYY-MM-DD | [@name] | Example: Quiet hours and per-user frequency caps |
| 1.0 | YYYY-MM-DD | [@name] | Example: Approved |
Contents
1. Overview
1.1 Problem statement
[Example: Nine product teams call email, SMS and push providers directly. Users get the same alert three times, unsubscribe links work for only some emails, a marketing campaign once delayed password-reset emails by 40 minutes, and nobody can answer "did this user receive the message?".]
1.2 Proposed solution
[Example: Product teams publish a notification request (recipient, template, data, category, priority) to one API. The service deduplicates the request, checks the user's preferences, quiet hours and frequency caps, renders the template per channel and locale, and places one job per channel on priority queues. Channel workers send through the primary provider with retries and fail over to a secondary provider. Provider callbacks and webhooks update a delivery log that support and product teams can query.]
1.3 Decisions requested from reviewers
| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | Example: Separate infrastructure for one-time codes? | Yes: own queue, workers and SMS route | 7.2 |
| Q-02 | Example: Who owns templates? | Product teams, reviewed in a template registry | 7.1 |
| Q-03 | Example: Deduplication window? | 24 h by caller key; 10 min by content hash | 7.5 |
| Q-04 | Example: Retention of delivery logs? | 90 days detailed, 13 months aggregated | 5.2 |
| [Q-nn] | [Question] | [Option] | [Section] |
2. Requirements
2.1 Functional requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-01 | Example: Send via push (iOS, Android, web), email, SMS and in-app inbox | Must |
| FR-02 | Example: Versioned, localised templates per channel | Must |
| FR-03 | Example: Respect per-category, per-channel preferences and global opt-out | Must |
| FR-04 | Example: Quiet hours in the user's time zone for non-urgent categories | Should |
| FR-05 | Example: Per-user frequency caps (for example 3 marketing pushes per day) | Must |
| FR-06 | Example: Fallback channel when a channel is unavailable for the user | Should |
| FR-07 | Example: Delivery status per notification and channel, queryable by support | Must |
| FR-08 | Example: Scheduled and batched sends (digests, campaigns) | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Latency (critical) | Example: One-time code handed to provider within 2 s p99 | Accept-to-provider timing |
| NFR-02 | Latency (normal) | Example: Transactional within 30 s p99 | Same |
| NFR-03 | Availability | Example: 99.95% for the accept API | Synthetic checks |
| NFR-04 | No loss | Example: Every accepted request reaches a final state (sent, suppressed or failed) | Reconciliation of states |
| NFR-05 | No duplicates | Example: Duplicate deliveries below 0.01% | Dedup audit |
| NFR-06 | Compliance | Example: Unsubscribe honoured within 24 h at most, target 1 min | Preference audit |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |
2.3 Out of scope
- [Example: campaign authoring UI and audience segmentation]
- [Example: two-way SMS conversations]
- [Example: voice calls]
- [Item deferred, with the release that will take it]
3. Capacity estimation
3.1 Assumptions
| Input | Symbol | Value | Source |
|---|---|---|---|
| Notification requests per day | Example: 500 million | Sum of team forecasts | |
| Channel mix: push / email / in-app / SMS | Example: 70% / 20% / 8% / 2% | Current usage | |
| Campaign burst factor | Example: 10 (campaigns released at once) | Marketing calendar | |
| Delivery log record incl. status events | Example: 1 KB | Schema in 5.2 | |
| Detailed log retention | Example: 90 days | Decision Q-04 | |
| SMS price per message | Example: $0.0075 average | Provider contracts |
3.2 Throughput
Per channel at burst:
| Channel | Share | Burst rate | Implication |
|---|---|---|---|
| Push | 70% | ~40 600/s | Provider connections pooled; batch APIs where offered |
| 20% | ~11 600/s | Provider send rate contracts; warm-up of sending IPs and domains | |
| In-app | 8% | ~4 600/s | Own store write |
| SMS | 2% | ~1 160/s | Provider throughput per sender ID; cost control |
Bursts come from campaigns, which are low priority, so they can be smoothed over minutes without breaking any requirement.
3.3 Storage
3.4 SMS cost
This is the dominant cost and justifies push-first fallback ordering and strict SMS eligibility rules (7.2).
3.5 Summary
| Quantity | Estimate | Design consequence |
|---|---|---|
| Requests | ~5 800/s average, ~58 000/s campaign burst | Priority queues; campaigns throttled |
| Delivery log | ~45 TB (90 days), 135 TB raw | Wide-column store with TTL |
| SMS cost | ~$2.25M/month at assumed mix | Channel policy and fallback order are cost controls |
4. API design
4.1 Endpoints
| Method | Path | Purpose | Auth | Idempotent |
|---|---|---|---|---|
| POST | /v1/notifications | Request a notification for one user | Service token | Required Idempotency-Key |
| POST | /v1/notifications/batch | Up to 1 000 recipients, same template | Service token | Required Idempotency-Key |
| GET | /v1/notifications/{id} | Status per channel with events | Service or support token | Yes |
| GET / PUT | /v1/users/{id}/preferences | Read or update preferences | User or service token | Yes |
| GET | /v1/users/{id}/inbox?cursor= | In-app inbox | User token | Yes |
| POST | /v1/providers/{p}/callbacks | Provider delivery events | Provider signature | Dedup by provider event ID |
4.2 Request and response examples
A product team asks for a transactional notification; the service decides channels from the category and user preferences:
POST /v1/notifications HTTP/1.1
Content-Type: application/json
Idempotency-Key: order-shipped:ord_88123
{"userId": "u_4410", "category": "order_updates", "priority": "transactional", "template": "order_shipped", "templateVersion": 3, "data": {"orderId": "ord_88123", "carrier": "DHL", "eta": "2026-03-16"}, "channels": ["push", "email"]}
HTTP/1.1 202 Accepted
Content-Type: application/json
{"id": "ntf_01JA9", "state": "accepted", "dedupOf": null}
4.3 Errors and limits
| Status | Problem type | When |
|---|---|---|
| 400 | template-data-invalid | Data does not match the template's schema |
| 404 | template-not-found | Template or version missing or not approved |
| 409 | duplicate | Same idempotency key with different body |
| 429 | caller-rate-limited | Calling service exceeded its quota |
Suppression by preference is not an error: the request is accepted and its
final state becomes suppressed with a reason.
5. Data model
5.1 Entities
A notification request produces one delivery per channel; each delivery records attempts and provider events.
5.2 Storage choices
| Data | Store | Key | Main queries | Why |
|---|---|---|---|---|
| Templates | Relational database + cache | template_id, version | Get approved version | Small, reviewed, versioned |
| Preferences, contact points | Relational database + cache | user_id | All preferences for a user | Consistency for opt-out |
| Notifications, deliveries, events | Example: Wide-column store with 90-day TTL | notification_id; secondary by user_id + day | Status by ID; history by user | High write rate, time-bounded |
| Dedup keys | In-memory store with TTL | caller:idem_key, content hash | Exists? | Fast check, short lifetime |
| In-app inbox | Wide-column store | user_id, clustered by time | Latest items, unread count | Per-user range reads |
| Queues | Durable message broker | Topic per channel and priority | Consume in order of priority | Buffer bursts, retries |
6. High-level design
The accept path is short and synchronous; everything after the queue is asynchronous and retryable.
| Component | Responsibility | Scales by |
|---|---|---|
| Notification API | Authenticate caller, validate, dedup by key, enqueue, return 202 | Stateless replicas |
| Orchestrator | Preferences, caps, quiet hours, channel selection, rendering, per-channel jobs | Consumer partitions by user_id |
| Channel workers | Send through providers, retry, fail over, record attempts | Worker pools per channel and priority |
| Callback receiver | Verify provider signatures, map events to delivery states | Stateless replicas |
| [Component] | [Responsibility] | [Scaling unit] |
7. Detailed design
7.1 Templates
Templates are stored per channel and locale in a registry, written in a logic-less language (variables, conditionals, loops only), validated against a JSON Schema for their data, and require review before activation. Email templates are rendered to HTML and plain text; push templates are checked against platform payload limits; SMS templates are checked for segment count, since a message above 160 GSM-7 characters (70 in UCS-2) is billed as several segments.
7.2 Priorities and channel policy
| Priority | Examples | Queue | Quiet hours | Caps | Channels |
|---|---|---|---|---|---|
| P0 critical | One-time codes, security alerts | Dedicated, never shared with campaigns | Ignored | Abuse limits only | SMS or email as requested; push for app approvals |
| P1 transactional | Receipts, shipping, mentions | Shared transactional | Respected for push, not email | Per category | Push first, email fallback |
| P2 marketing | Campaigns, digests, recommendations | Throttled | Respected | Strict (Example: 3 push/day, 1 email/day) | Only with explicit opt-in where required |
Workers consume P0 first, then P1, then P2, with P2 limited to a configurable share of provider capacity so campaigns can never starve transactional mail.
7.3 Preferences, consent and opt-out
- Preferences are stored per category and channel, defaulting by category (marketing off unless consent is recorded where the market requires it).
- Every email carries a one-click unsubscribe (the
List-UnsubscribeandList-Unsubscribe-Postheaders per RFC 8058) and SMS honours STOP keywords through the provider's opt-out handling. - Opt-out writes are synchronous to the preference store, and the orchestrator reads preferences at send time, not at request time, so a scheduled campaign respects an unsubscribe made after scheduling.
- Hard bounces and invalid push tokens mark the contact point invalid.
7.4 Send path with retries and failover (critical path)
The sequence below shows a transactional push with an email fallback.
Retry delay for attempt uses capped exponential backoff with full jitter:
With base 1 s and cap 5 min, attempt 6 waits up to 64 s and attempt 9 up to 5 minutes. P0 jobs use base 200 ms and give up after 30 s, because a late one-time code is useless; the user can request another.
Provider failover uses a circuit breaker per provider and region: when the
error rate exceeds 20% over 1 minute, new sends route to the secondary until
probes succeed. Only errors that are safe to retry (timeouts, 5xx, throttling)
trigger a retry; permanent errors (invalid number, unsubscribed address) end
the delivery as failed.
7.5 Deduplication
| Layer | Key | Window | Catches |
|---|---|---|---|
| Caller idempotency | caller:Idempotency-Key | 24 h | Retries by the calling service |
| Content hash | user + template + data hash | 10 min | Two services emitting the same event |
| Worker send | delivery_id + attempt with provider idempotency where offered | Per delivery | Worker crash after send, before ack |
A residual duplicate is possible when a provider accepts a send but the response is lost and the provider offers no idempotency. This is accepted and measured (NFR-05).
7.6 Delivery tracking and consistency model
Each delivery moves through states driven by workers and provider callbacks; callbacks can arrive late or out of order, so transitions only move forward.
The delivery log is eventually consistent with providers: "delivered" may appear minutes after "sent", and some channels (SMS in some countries, push on some platforms) never report delivery. Preferences are strongly consistent (read from the primary on opt-out paths). Open tracking in email is approximate because many clients pre-fetch or block images.
8. Scalability and performance
| Concern | Approach | Limit before redesign |
|---|---|---|
| Campaign bursts | P2 throttled to share of capacity; scheduled release over a window | Provider contract rates |
| Provider rate limits | Token buckets per provider account in workers | Contracted throughput |
| Per-user caps | Counters per user and category in in-memory store with daily TTL | [Limit] |
| Hot users (shared accounts, bots) | Abuse limits per recipient | 100 notifications/hour per user |
| Delivery log writes | Wide-column store, TTL, partition by day | Example: ~200 000 writes/s per cluster |
Latency budget for a P0 one-time code (target 2 s to provider):
| Step | Budget |
|---|---|
| API accept and dedup | 50 ms |
| Intake to orchestrator | 200 ms |
| Preferences and rendering | 100 ms |
| P0 queue to worker | 200 ms |
| Provider API call | 800 ms |
| Headroom | 650 ms |
9. Reliability, security and operations
9.1 Failure modes
| Failure | Impact | Detection | Mitigation |
|---|---|---|---|
| Primary SMS provider outage | Codes not delivered | Circuit breaker, delivery receipts drop | Failover to secondary route per country |
| Email IP reputation drop | Mail to spam | Inbox placement and complaint rates | Separate IPs and domains for transactional and marketing |
| Queue backlog | Delays | Age of oldest message per queue | Autoscale workers; shed P2 first |
| Preference store unavailable | Cannot check consent | Error rate | P2 paused (fail closed); P0 and P1 proceed with cached preferences |
| Callback flood | Log write pressure | Callback rate | Buffer in queue; batch writes |
| [Failure] | [Impact] | [Signal] | [Mitigation] |
9.2 Security, privacy and abuse
| Threat | Control |
|---|---|
| Phishing using our domains | SPF, DKIM and DMARC with enforcement; BIMI optional |
| SMS pumping fraud (fake numbers generating paid traffic) | Per-country allowlists, velocity limits on code requests, anomaly alerts on cost |
| Sensitive data in notifications | Template review; no secrets in push previews; codes only in P0 templates |
| Forged provider callbacks | Signature verification and replay protection |
| Data retention | Delivery logs deleted after 90 days; message bodies not stored after send unless required |
| [Threat] | [Control] |
9.3 Observability
| Signal | Metric or log | Alert threshold |
|---|---|---|
| P0 latency | Accept to provider p99 | Above 2 s for 5 min |
| Queue age | Oldest message per priority | P1 above 30 s, P2 above 30 min |
| Provider health | Error rate per provider and country | Above 5% for 5 min |
| Delivery rate | Delivered / sent per channel | Drop of 5 points vs. baseline |
| Spam complaints | Complaints / delivered email | Above 0.1% |
| SMS spend | Cost per hour per country | Above 2x forecast |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly cost |
|---|---|---|---|
| SMS | Example: $0.0075 average | Example: 300 million | Example: $2.25 million |
| [$/1 000] | Example: 3 billion | [$] | |
| Push | Platform services usually free | Example: 10.5 billion | [$] |
| Delivery log storage | [$/TB-month] | Example: 135 TB raw | [$] |
| Compute and queues | [$/month] | [n] | [$] |
| Total | [$] |
10. Trade-offs and alternatives
| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Ownership | Each team integrates providers; central service | Central service | One place for consent, caps and tracking | Teams depend on platform roadmap |
| Queueing | Single queue; per-channel; per-channel and priority | Per channel and priority | Campaigns cannot delay codes | More queues to operate |
| Preference check time | At request; at send | At send | Honours late opt-outs | Extra read per send |
| Providers | Single per channel; multi-provider with failover | Multi-provider for SMS and email | Outages and country coverage | Integration and contract overhead |
| Delivery guarantee | At-most-once; at-least-once with dedup | At-least-once with dedup | No silent loss of important messages | Rare duplicates remain possible |
| Build or buy | Hosted notification platform; in-house | [In-house / Hosted] | [Reason: cost at volume, data residency, control] | [Cost] |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | API, email and push, preferences, delivery log | Two teams migrated; zero lost requests in reconciliation | YYYY-MM-DD |
| M2 | P0 path with SMS and failover | Failover drill under 1 min | YYYY-MM-DD |
| M3 | Caps, quiet hours, in-app inbox | Complaint rate down 30% for migrated teams | YYYY-MM-DD |
| M4 | All teams migrated; direct provider keys revoked | No direct provider traffic for 30 days | YYYY-MM-DD |
11.2 Rollout and rollback
[Example: Migrate one category at a time behind a flag in the calling service. During migration, the old path stays available; rollback flips the flag. Provider credentials for direct use are revoked only after M4.]
11.3 Risks
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | Example: Central service outage stops all notifications | Low | High | Multi-zone, P0 isolation, runbooks | [@name] |
| R-02 | Example: SMS fraud spikes cost | Medium | High | Country allowlist, velocity limits, spend alerts | [@name] |
| R-03 | Example: Teams bypass the platform | Medium | Medium | Revoke direct credentials; good SDK | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Open questions
| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | Example: Do we need per-tenant sender domains for B2B customers? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: Which countries require pre-registered SMS sender IDs? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.2 Glossary
| Term | Meaning |
|---|---|
| Transactional | Message triggered by a user action or account event |
| Suppression | Decision not to send because of preference, cap or invalid contact |
| Hard bounce | Permanent email delivery failure |
| Circuit breaker | Mechanism that stops calls to a failing dependency for a time |
| Quiet hours | User-local period when non-urgent notifications are held |
| [Term] | [Meaning] |
12.3 References
| Reference | Use |
|---|---|
| RFC 8058 Signaling One-Click Functionality for List Email Headers | One-click unsubscribe |
| RFC 7489 DMARC; RFC 7208 SPF; RFC 6376 DKIM | Email authentication |
| Apple Push Notification service, Firebase Cloud Messaging, Web Push (RFC 8030) | Push channels |
| [Provider documentation and contracts] | Rates and callbacks |
Standard and licence note
Based on common industry practice for notification platforms and on the specifications cited in 12.3. This is a reference design to adapt, with illustrative numbers and prices; it is a format, not legal advice on consent or marketing rules. Original text written for this collection. Template licence: MIT.
相关模板
Database Design Document Template (ERD and Physical Model)
Database design: DBMS choice, conceptual, logical and physical models, indexes, partitioning, integrity, security, backup, capacity
架构与系统设计Chat System Design Template (Reference Design)
Real-time 1:1 and group chat: WebSocket gateways, per-conversation ordering, fan-out, receipts, offline sync, push, E2EE
架构与系统设计Collaborative Editing System Design Template (Reference Design)
Real-time co-editing: OT vs CRDT, sequence CRDT, state-vector sync, presence, offline merge, snapshots and compaction, access