# 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.

<!--
Guidance: How to use this template
- Separate transactional messages (password reset, one-time codes, receipts)
  from marketing messages from the first page. They differ in priority,
  consent rules, rate limits and providers.
- Fill the channel mix in 3.1 from real data: SMS volume drives cost,
  push volume drives throughput.
- Treat every provider as unreliable and every send as possibly duplicated;
  sections 7.4 and 7.5 show the defences.
- Have legal or privacy staff review 7.3 (preferences and consent) for each
  market you send to.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| 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

<!-- Guidance: Note what changed in the design, not the wording. -->

| 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

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, and the decisions that need their attention. -->

### 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

<!-- Guidance: Functional requirements describe behaviour; non-functional requirements describe qualities with numbers. Mark what is out of scope so nobody designs for it. -->

### 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

<!-- Guidance: Back-of-the-envelope numbers decide the shape of the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Notification requests per day | $N$ | *Example:* 500 million | Sum of team forecasts |
| Channel mix: push / email / in-app / SMS | | *Example:* 70% / 20% / 8% / 2% | Current usage |
| Campaign burst factor | $k$ | *Example:* 10 (campaigns released at once) | Marketing calendar |
| Delivery log record incl. status events | $s$ | *Example:* 1 KB | Schema in 5.2 |
| Detailed log retention | $T$ | *Example:* 90 days | Decision Q-04 |
| SMS price per message | $p_{sms}$ | *Example:* $0.0075 average | Provider contracts |

### 3.2 Throughput

$$
QPS_{avg} = \frac{5 \times 10^{8}}{86\,400} \approx 5\,800 \ \text{req/s}, \qquad QPS_{burst} = k \times QPS_{avg} \approx 58\,000 \ \text{req/s}
$$

Per channel at burst:

| Channel | Share | Burst rate | Implication |
|---|---|---|---|
| Push | 70% | ~40 600/s | Provider connections pooled; batch APIs where offered |
| Email | 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

$$
S = N \times s \times T = 5 \times 10^{8} \times 1 \ \text{KB} \times 90 = 45 \ \text{TB}, \qquad \text{RF} = 3: 135 \ \text{TB raw}
$$

### 3.4 SMS cost

$$
C_{sms} = 0.02 \times 5 \times 10^{8} \times 30 \times 0.0075 \approx \$2.25 \ \text{million/month}
$$

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

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 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:

```http
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
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

<!-- Guidance: Choose storage by access pattern, not by habit. Show entities, keys, partitioning and the queries each store must answer. -->

### 5.1 Entities

A notification request produces one delivery per channel; each delivery
records attempts and provider events.

```mermaid
erDiagram
  TEMPLATE {
    string template_id PK
    int version PK
    string category
    string channel
    string locale
    string body_source
    string status
  }
  PREFERENCE {
    string user_id PK
    string category PK
    string channel PK
    boolean enabled
    datetime updated_at
  }
  NOTIFICATION {
    string notification_id PK
    string user_id
    string category
    string priority
    string idem_key
    string state
    datetime created_at
  }
  DELIVERY {
    string delivery_id PK
    string notification_id FK
    string channel
    string provider
    string provider_msg_id
    string state
    int attempts
  }
  DELIVERY_EVENT {
    string delivery_id PK
    datetime at PK
    string event
    string detail
  }
  NOTIFICATION ||--|{ DELIVERY : "fans out to"
  DELIVERY ||--o{ DELIVERY_EVENT : records
  TEMPLATE ||--o{ NOTIFICATION : renders
```

### 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

<!-- Guidance: One diagram that shows every component on the request paths and where state lives. Name components by responsibility. -->

The accept path is short and synchronous; everything after the queue is
asynchronous and retryable.

```mermaid
flowchart LR
  teams([Product services]) --> api[Notification API]
  api --> dedup[(Dedup store)]
  api --> inq[[Intake queue]]
  inq --> orch[Orchestrator]
  orch --> prefs[(Preferences)]
  orch --> tmpl[(Templates)]
  orch --> caps[Frequency caps and quiet hours]
  orch --> q0[[P0 queue: codes, security]]
  orch --> q1[[P1 queue: transactional]]
  orch --> q2[[P2 queue: marketing, digests]]
  q0 --> w[Channel workers]
  q1 --> w
  q2 --> w
  w --> pp([Push providers])
  w --> ep([Email providers])
  w --> sp([SMS providers])
  w --> inbox[(In-app inbox)]
  pp --> cb[Callback receiver]
  ep --> cb
  sp --> cb
  cb --> log[(Delivery log)]
  w --> log
```

| 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

<!-- Guidance: Go deep on the parts that are new, risky or contested. Show the sequence of calls and the failure behaviour of each step. -->

### 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-Unsubscribe` and
  `List-Unsubscribe-Post` headers 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.

```mermaid
sequenceDiagram
  participant S as Product service
  participant A as Notification API
  participant D as Dedup store
  participant O as Orchestrator
  participant P as Preferences
  participant W as Push worker
  participant X as Push provider
  participant E as Email worker
  participant L as Delivery log
  S->>A: POST /notifications (Idempotency-Key)
  A->>D: SETNX caller:key (TTL 24 h)
  D-->>A: new
  A-->>S: 202 accepted
  A--)O: intake event
  O->>P: preferences, quiet hours, caps
  P-->>O: push allowed, email allowed
  O->>O: render templates
  O->>W: push job (P1)
  W->>X: send
  alt accepted by provider
    X-->>W: message id
    W->>L: state sent
  else provider error or timeout
    W->>W: retry with backoff, then secondary provider
    W->>E: fallback email job after final failure
    E->>L: state sent via email
  end
```

Retry delay for attempt $n$ uses capped exponential backoff with full jitter:

$$
\text{delay}_n = \text{rand}\left(0,\ \min(\text{cap},\ \text{base} \times 2^{n})\right)
$$

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.

```mermaid
stateDiagram-v2
  [*] --> accepted
  accepted --> suppressed: preference, cap or invalid contact
  accepted --> queued
  queued --> sending
  sending --> retrying: retryable error
  retrying --> sending: backoff elapsed
  sending --> sent: provider accepted
  retrying --> failed: attempts exhausted
  sending --> failed: permanent error
  sent --> delivered: provider receipt
  sent --> bounced: bounce or undeliverable
  delivered --> opened: open or click (where tracked)
  suppressed --> [*]
  failed --> [*]
  bounced --> [*]
  opened --> [*]
```

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

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| 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

<!-- Guidance: Name the failure modes, the threats and the signals operators watch. Each mitigation should be testable. -->

### 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 |
| Email | [$/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

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Reviewers will propose the alternatives anyway; answer them here. -->

| 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

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 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

<!-- Guidance: Supporting detail, glossary and open questions. -->

### 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.
