通知システムの設計

プッシュ・SMS・メールを1本のAPIで束ね、億通のファンアウトでも重複なく届く通知基盤を設計できるようになる。プロバイダ抽象化・優先度キュー・冪等な再送・オプトアウトまでを一気通貫で解説する。

応用システム設計通知メッセージキュー冪等性ファンアウト最終更新: 2026-07-29
3つの要点
TL;DR
  1. 通知APIは1本に束ね、チャネル差はプロバイダアダプタの背後へ隠す。プッシュ/SMS/メールは配信特性(レイテンシ・コスト・失敗率)が桁で違うため、優先度別のキューに振り分けてレート制御する。
  2. 外部プロバイダは一時失敗が常態。指数バックオフ+ジッタで再送し、送信要求に冪等キーを持たせて二重送信を防ぐ。DLQに落ちたものだけを隔離・再処理する。
  3. 億単位のファンアウトはキューで非同期化し、消費者を水平スケールさせる。オプトアウトは配信直前に必ず再チェックし、テンプレートはロケール・チャネル別に管理してレンダリングを送信から分離する。

通知システムは「イベントを受け取り、対象ユーザーへ適切なチャネルで届ける」だけに見えて、外部プロバイダの不安定さ・億単位のファンアウト・重複配信の抑止・オプトアウトの厳守が絡み、設計判断の宝庫になります。ここではシステム設計面接の解答フォーマットに沿って、要件定義から具体的な数値見積もり、内部アーキテクチャ、スケール時のトレードオフまでを順に組み立てます。

要件と規模の見積もり

まず機能要件と非機能要件を分けます。機能要件は「イベントを起点に、プッシュ通知・SMS・メールの複数チャネルへ配信する」「ユーザーの購読設定とオプトアウトを尊重する」「テンプレートとロケールに基づき本文を生成する」「配信状況(送信済み・失敗・開封)を追跡する」の4点です。非機能要件が本質で、(1) 高スループット(大量ファンアウトを捌く)、(2) チャネルに応じた配信レイテンシ(トランザクション系のOTPは秒単位、マーケ系は分〜時間で可)、(3) 少なくとも1回の配信保証(at-least-once)と、その裏返しである重複の抑止、(4) 外部プロバイダ障害への耐性、を満たす必要があります。

規模感をフェルミ推定で押さえます。DAU 1,000万、1人あたり1日平均5通知を送るとします。日次は 1e7 × 55,000万通/日。1日は約86,400秒なので平均は 5e7 / 86400、おおよそ 約580通/秒。ピークは平均の10倍と見積もって 約6,000通/秒 とします。ここに「マーケティングの一斉配信(キャンペーン)」が重なると桁が跳ねます。全1,000万ユーザーへ一斉送信するキャンペーンを10分で流し切るなら 1e7 / 600約1.7万通/秒 の瞬間ファンアウトになり、これが定常負荷ではなくスパイクとして効いてくる点が設計を決めます。

ストレージを見積もります。配信ログを1通あたり「通知ID・ユーザーID・チャネル・状態・タイムスタンプ」で約200バイトとすると、5e7 × 200B1日あたり約10GB、90日保持で 約900GB。テンプレートや購読設定は桁が小さく、支配的なのは配信ログです。この「書き込みは膨大だが1件は軽量、読み取りは調査時に限られる」特性が、ログを時系列で追記しやすいストアに寄せる判断根拠になります。

面接での桁の出し方

QPSは「DAU × 1人あたり通知数 → 秒あたり平均 → ピーク倍率」で出し、そこへ一斉配信のスパイクを別立てで加えるのが要点です。定常6,000通/秒に対しキャンペーンで1.7万通/秒という「山」を示せると、なぜキューでの平滑化と消費者の水平スケールが要るのか、という後段の結論に自然につながります。

大枠の設計

外部インタフェースは1本に束ねます。チャネルごとにAPIを分けず、単一の送信エンドポイントでチャネルを引数に取るのが肝です。

POST /v1/notifications
body: {
  userId, template, locale,
  channels: ["push","sms","email"],   # 希望チャネル(優先順)
  data: { ... },                       # テンプレートへ差し込む変数
  idempotencyKey                       # 二重送信防止キー
}
-> { notificationId, accepted: bool }

呼び出し側はチャネルの実装差を一切意識しません。全体の流れはこうです。(1) 通知APIが要求を受理し、オプトアウトと購読設定を照合、(2) テンプレートをレンダリングし、(3) チャネル別の優先度キューへ投入、(4) チャネルごとのワーカー(消費者)がキューから引き、(5) プロバイダアダプタが外部サービス(FCM/APNs・SMSゲートウェイ・メール送信基盤)を叩き、(6) 結果を配信ログへ記録、という段構えです。APIとワーカーをキューで分離するのは、受理の応答を外部プロバイダの遅延・障害から切り離し、ピークをキューで吸収するためです。

設計の心臓部はプロバイダ抽象化です。チャネルごとに配信特性がまるで違うため、共通インタフェースの背後に個別実装を隠します。

チャネルレイテンシ目安コスト失敗率・特性冪等性の担保
プッシュ(FCM/APNs)秒未満ほぼ無料トークン失効が多い。到達通知は限定的プロバイダのメッセージIDで判定
SMS数秒1通ごとに高コストキャリア依存で不達あり。順序保証弱い送信要求に冪等キーを付与
メール秒〜分安価スパム判定・バウンス。開封追跡は不確実Message-IDとキューの重複排除

共通インタフェースは send(message) -> {status, providerMessageId} に統一し、アダプタが各プロバイダのAPI形式・認証・エラーコードの差を吸収します。新チャネル(例 LINE・Slack・Webhook)の追加は、このインタフェースを実装したアダプタを足すだけで済み、上位のパイプラインは無変更、という拡張性がこの抽象化の狙いです。

主要コンポーネントの深掘り

優先度キューとレート制御

すべての通知を同じ列に並べると、緊急のOTP(ワンタイムパスワード)が、その前に積まれた数百万通のマーケメールの後ろで待たされます。これを避けるため、優先度別に物理キューを分離します。素朴な優先度付きヒープを1本持つより、high(OTP・二要素認証・重要アラート)・default(取引通知)・low(マーケ・ダイジェスト)の3本を用意し、消費者が高優先度から先に引く方が、分散環境で実装が単純かつ枯渇制御しやすくなります。低優先度が永久に後回しになるスタベーションは、高優先度が空のときだけ低を引く単純規則か、優先度ごとに消費者数を配分することで防ぎます。

レート制御は2つの理由で必須です。第一に外部プロバイダ側のAPIレート上限(例 SMSゲートウェイが毎秒N通まで)を超えないため、第二に受信側を守るため(同一ユーザーへ短時間に大量送信しない、いわゆる通知の集約)です。プロバイダ単位のスループットは、消費者の並列度とトークンバケットで絞ります。トークンバケットは「毎秒 rate 個のトークンが補充され、送信ごとに1個消費し、空なら待つ」モデルで、バースト許容量を独立に決められるのが利点です。アルゴリズムとLuaによるアトミック実装の詳細はレートリミッタ側の議論に譲りますが、複数消費者で1つの上限を共有するため、カウンタは共有ストアに集約します。

なぜ優先度でキューを物理分割するのか

1本のキューに優先度フィールドを持たせて並べ替える方式は、キュー実装(多くは先入れ先出し)に高コストな再ソートを強い、ヘッドオブラインブロッキングも起きやすくなります。用途別に物理キューを分ければ、それぞれ独立にスケール・レート制御・監視でき、低優先度の一斉配信が高優先度を巻き込む事故を構造的に防げます。

リトライと冪等性

外部プロバイダは一時失敗が常態です。タイムアウト・5xx・レート超過(429)は、時間を置けば成功しうる一過性エラーなので再送します。ただし即時リトライの群れは回復直後に再スパイクを起こす(サンダリングハード)ため、指数バックオフ+ジッタで間隔をばらけさせます。

attempt 1: 即時
attempt 2: 2秒 ± ジッタ
attempt 3: 4秒 ± ジッタ
attempt 4: 8秒 ± ジッタ
...上限回数に達したら DLQ(デッドレターキュー)へ

一方、トークン失効・不正な宛先・恒久的なバウンスといった恒久エラーは再送しても無駄なので、即座に失敗確定させてログに残します。一過性か恒久かのエラー分類が、無駄なリトライと確実な失敗記録を分ける鍵です。

再送が入る以上、冪等性が不可欠です。少なくとも1回配信(at-least-once)を保証するキューでは、消費者がメッセージ処理後にACKを返す前にクラッシュすると、同じメッセージが再配送され、二重送信が起きます。これを抑えるには、送信要求ごとに一意な冪等キーを持たせ、送信前に「このキーは処理済みか」を共有ストアで確認します。

# 消費者の冪等な送信
key = message.idempotencyKey
if store.setnx(key, "sending", ttl):   # 未処理なら予約(アトミック)
    result = provider.send(message)
    store.set(key, result.status, ttl)
else:
    skip   # 既に処理中/処理済み。二重送信を回避

setnx(存在しなければ設定、の原子操作)で「予約」をアトミックに取るのが要点です。プロバイダ側も冪等キーを受け付けるものが多く(例 メールの Message-ID、SMSの client reference)、二重防御として渡します。これにより、キューの再配送や消費者の再起動が起きても、ユーザーに届く通知は1回に保てます。at-least-once配信を、宛先での実質exactly-onceへ寄せる標準的な手法です。

冪等キーのTTLとレース

冪等キーのTTLが短すぎると、遅れて再配送されたメッセージがキー失効後に届き二重送信になります。逆に長すぎると正規の再送信(同一内容を意図的に再度送る運用)まで弾きます。窓は「最大再送遅延+余裕」で設定します。また setnx で予約した後に送信中クラッシュするとキーが sending のまま残るため、sending 状態には短めのTTLを付け、タイムアウトしたら再処理可能に戻す設計が要ります。

テンプレートとオプトアウト

本文生成は送信から分離します。テンプレートはチャネル別・ロケール別に管理し(プッシュは短文、メールはHTML、SMSは文字数制限あり)、送信時に変数を差し込んでレンダリングします。テンプレートをコードから外部化するのは、文言変更のたびにデプロイせず、非エンジニアも編集でき、多言語展開が差分だけで済むためです。レンダリング結果を配信ログに残すと、後から「何を送ったか」を再現できます。

オプトアウト(配信停止)は法令(メールのCAN-SPAM・各国のプライバシー規制)とユーザー体験の両面で必須です。設計上の鉄則は、配信の直前に必ず購読状態を再チェックすることです。API受理時にチェックしても、キューで待つ間にユーザーが配信停止するケースがあるため、消費者が送信する寸前にもう一度照合します。チャネル別・カテゴリ別(取引通知は継続するがマーケは停止、など)に粒度を持たせ、取引系のクリティカルな通知はオプトアウト対象外とするのが一般的です。

ボトルネックとトレードオフ

横にスクロール

通知依頼を展開ジョブと優先度キューへ分け送信事業者の障害を制御する図
大量ファンアウト、重複排除、回路遮断、DLQを責任境界ごとに整理します。

大量ファンアウトの増幅が最大の壁です。規模見積もりで見た通り、1,000万ユーザーへの一斉キャンペーンは瞬間1.7万通/秒のスパイクを生みます。これを同期処理すると通知APIが飽和するため、(1) キャンペーンを1件の「ファンアウトジョブ」として受理し、(2) 対象ユーザーをバッチに分割して個別の通知メッセージへ展開し、(3) キューへ非同期投入、という段階を踏みます。消費者は水平スケールし、キューの深さ(滞留数)をオートスケールのシグナルにします。キュー投入とプロバイダ送信を分けることで、外部プロバイダのレート上限に律速されても、キューが緩衝材となり受理側は倒れません。この非同期ファンアウトと配信保証の設計はメッセージキューの配送セマンティクスの議論と地続きです。

配信保証と重複排除のトレードオフが本質的な設計判断です。厳密なexactly-once配信を分散環境で保証するのは高コストで、CAP定理が示す通り分断時には整合性か可用性を諦めることになります。現実解は「キューはat-least-onceで確実に届け、宛先での重複は冪等キーで排除する」構えです。これは「配送は緩く多めに、受け取りで絞る」という発想で、通知が1通も欠落しないこと(可用性)を、多少の内部重複を許容ロジックで吸収することで両立させます。重複排除に使う冪等キーの共有ストアは、それ自体がホットスポットになり得るため、キー空間で分散させます。分散カウンタや状態をシャードへ均す考え方はコンシステントハッシュ法が基礎になります。

プロバイダ障害への耐性も設計に織り込みます。単一のSMSゲートウェイに依存すると、そこが落ちれば全SMSが止まります。対策は、(1) 同一チャネルに複数プロバイダを用意し、一方が失敗率を上げたらフェイルオーバーする、(2) 特定プロバイダが連続失敗したらサーキットブレーカで一時的に送信を止め、無駄なリトライで自分と相手を痛めつけない、(3) それでも送れないものはDLQに隔離し、復旧後にまとめて再処理する、の3層です。DLQを別立てにするのは、少数の毒メッセージ(何度送っても失敗する宛先)が正常な流れを詰まらせるのを防ぐためです。

課題素朴な方式本設計の対策トレードオフ
一斉配信スパイク同期でその場送信ファンアウトジョブ+キュー緩衝配信に遅延が乗る(マーケは許容)
二重送信そのまま再送冪等キーで宛先重複排除キー用ストアと若干の遅延
プロバイダ障害失敗したら諦め多重化+サーキットブレーカ+DLQ運用と構成の複雑化
優先度逆転単一キュー優先度別に物理キュー分離キュー数ぶんの管理コスト

最後に配信状態の追跡です。各通知は「受理→レンダリング済→キュー投入→送信済→配信確認/失敗」と状態遷移し、これを配信ログに時系列で記録します。プッシュやメールの到達・開封はプロバイダからのコールバック(Webhook)で非同期に返るため、送信時刻に確定せず、後追いで状態を更新する前提で設計します。ログの書き込みは膨大なので、追記に強いストアへ寄せ、集計は非同期に回します。より広い分散システムの設計原則はデータベースネットワークDevOpsの各トピックも参照してください。

要点整理

通知APIは1本に束ねチャネル差はプロバイダアダプタへ隠す。優先度別に物理キューを分離し、トークンバケットでプロバイダ上限と受信側を守る。一過性エラーは指数バックオフ+ジッタで再送、恒久エラーは即失敗、送信は冪等キーで宛先重複排除しat-least-onceを実質exactly-onceへ寄せる。オプトアウトは配信直前に再チェック、テンプレートはチャネル・ロケール別に外部化。大量ファンアウトはキューで非同期化し消費者を水平スケール、障害は多重化・サーキットブレーカ・DLQで凌ぐ。

システム設計の記事ガイド

通知システムの設計を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

システム設計

比較で見る軸

難易度: advanced / カテゴリ: システム設計 / タグ数: 5

導入後に効く点

外部プロバイダは一時失敗が常態。指数バックオフ+ジッタで再送し、送信要求に冪等キーを持たせて二重送信を防ぐ。DLQに落ちたものだけを隔離・再処理する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
システム設計
タグ数
5

判断チェックリスト

  • 自社の用途が「システム設計 / 通知」に近いか確認する。
  • 強みである「通知APIは1本に束ね、チャネル差はプロバイダアダプタの背後へ隠す。プッシュ/SMS/メールは配信特性(レイテンシ・コスト・失敗率)が桁で違うため、優先度別のキューに振り分けてレート制御する。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

システム設計通知メッセージキュー冪等性ファンアウト