通知システムの設計:プッシュ、メール、SMS のアーキテクチャ

プッシュ、メール、SMS の通知システムを設計します。単一の API、設定とおやすみ時間、重複排除、チャネル別キュー、再試行、配信追跡。

· 創業者・開発者

公開日:

通知システムの設計:プッシュ、メール、SMS のアーキテクチャ

通知は 1 行のコードから始まります。注文が発送されたらメールを送る、というものです。1 年後、同じ会社は十数個のサービスからプッシュ、メール、SMS を送るようになり、ユーザーは同じ通知を 3 回受け取り、パスワードリセットのメールが届いたかどうか誰にもわからず、マーケティングのメッセージが深夜 3 時に人を起こしています。

通知システムは、何かが起きたと判断するコードと、メッセージを届けるプロバイダーの間に 1 つのサービスを置くことで、これを解決します。この記事ではそのサービスを設計します。API、送信前に走るチェック、再試行付きのチャネル別配信、そして追跡です。終盤の図で、全体の流れを示します。

要件

  • プロダクトサービスは、ユーザーがどのチャネルを有効にしているかを知らなくても、1 回の呼び出しで通知を送れる。
  • プッシュ(iOS と Android)、メール、SMS。言語ごとのテンプレートを持つ。
  • ユーザーはチャネルとトピックを管理でき、おやすみ時間が守られる。
  • 同じイベントから同じメッセージが 2 回生まれることはない。
  • すべてのメッセージに、送信済み、配信済み、バウンス、失敗といったステータスがある。
  • 遅いプロバイダーが 1 つあっても、ほかのチャネルは遅れない。

API:メッセージではなくイベントを送る

プロダクトサービスがメールを組み立てるべきではありません。事実と冪等キーを含むイベントを送ります。

POST /notifications
{
  "idempotencyKey": "order-8812-shipped",
  "userId": "u_7",
  "type": "order_shipped",
  "data": {"orderId": "8812", "carrier": "UPS", "eta": "2026-10-30"}
}

それ以外はすべて通知サービスの担当です。この種類の通知がどのチャネルを使うか、どのテンプレートか、どの言語か、そしてユーザーがそもそも受け取りたいかどうか。チームが文面を変えたり、ある種類の通知に SMS を追加したりしても、注文サービスは何も変わりません。

送信する前に

処理がチャネルに届く前に、API でこの順に 4 つのチェックを実行します。

  1. 重複を排除する。 冪等キーを TTL 付きで保存し、繰り返しを捨てます。上流のサービスでの再試行は普通のことで、それが重複としてユーザーに届いてはいけません。
  2. 設定を確認する。 ユーザーのチャネル、トピックごとの配信停止、おやすみ時間を調べます。パスワードリセットのようなセキュリティ通知は、たいていマーケティングの配信停止を無視して届けます。マーケティングが配信停止を無視することはありません。このルールは文書にしておきます。
  3. 量に上限を設ける。 ユーザー、チャネル、1 時間ごとのメッセージ数を制限します。500 件のイベントを発火させるバグが、500 件のプッシュ通知になってはいけません。
  4. 描画する。 通知の種類、チャネル、ユーザーの言語に合ったテンプレートを選び、イベントのデータで埋めます。

おやすみ時間にはユーザーのタイムゾーンが必要です。急ぎでない限り、メッセージは捨てずに保留し、時間帯の終わりに送るよう予約します。

チャネルごとに 1 つのキューとワーカープール

チェックのあと、API はチャネルごとに 1 件のジョブを、そのチャネルのキューに書き込みます。キューを分けることが、この設計で最も重要な判断です。SMS プロバイダーが遅くなっても、SMS のジョブは自分のキューにたまるだけで、プッシュとメールは流れ続けます。

チャネルのワーカーはプロバイダーを呼び出し、それぞれのクセに対応します。

  • プッシュ。 iOS 端末は Apple Push Notification service を、Android は FCM HTTP v1 API を通ります。デバイストークンは古くなります。APNs は有効でなくなったトークンに 410 を返し、FCM は unregistered エラーを返します。そうしたトークンは削除します。さもないと、誰にも届かないメッセージのために遅延を払い続けることになります。
  • メール。 プロバイダー経由で送り、バウンスと苦情を報告として受け取ります。一斉メールを送るなら、RFC 8058 で定義されたワンクリック配信停止のヘッダーを付けます。Gmail の 送信者のガイドライン は、1 日に 5,000 通を超えて送る送信者に対し、マーケティングと購読型のメッセージでワンクリック配信停止を求めています。
  • SMS。 メッセージあたり最も高価で、最も規制の多いチャネルです。認証コードと緊急の通知に限って使い、送信先の国ごとにルールを確認します(この記事は法的助言ではありません)。

ワーカーは一時的な失敗を指数バックオフとジッターで再試行し、決まった回数を超えたジョブはデッドレターキューに移します。無効なアドレスのような恒久的な失敗は再試行しません。

配信の追跡

プロバイダーは、何が起きたかを Webhook や応答コードで知らせてきます。受け付け、配信、バウンス、開封などです。ステータスの変化はすべて、通知 ID をキーにした配信ログに書き込みます。そうすれば、サポートは「ユーザーはリセットのメールを受け取ったか」に数秒で答えられ、チャネルとプロバイダーごとの配信率も測れます。

ステータスの更新は順不同で、ときには 2 回届きます。ステータスの列を上書きするのではなく、タイムスタンプ付きのイベントとして保存し、そこから現在の状態を導き出します。

アプリ内通知

多くの製品には、アプリ内の受信箱もあります。件数付きのベルのアイコンです。これは、プロバイダーを呼ぶ代わりにデータベースに書き込む 4 つ目のチャネルとして扱います。ユーザーと通知ごとに既読フラグ付きの行を保存し、ユーザーと時刻でインデックスを張り、アプリが最新のページと未読件数を取得できるようにします。既読はまとめて付けるようにして、受信箱を開いたユーザーが 50 回の書き込みを起こさないようにします。

ざっくりした見積もり

1 日のアクティブユーザーが 100 万人で、1 人あたり 1 日 5 件の通知なら、システムは 1 日に 500 万件、平均で毎秒約 58 件を送ります。重要なのはピークです。キャンペーンや障害のアラートは、数分で 100 万件のメッセージを送ることがあります。キューはバーストに合わせてサイズを決め、ワーカーには各プロバイダーが許す速度でキューを消化させます。そして、自分のワーカーの処理能力が尽きる前に、プロバイダーのほうが流量を絞ってくると考えておきます。

アーキテクチャ図

プロダクトサービスはイベントを通知 API に送ります。API は設定ストアを読み、プッシュ、メール、SMS の各キューに処理を振り分けます。各キューは専用のワーカーにつながり、ワーカーは APNs と FCM、メールプロバイダー、SMS プロバイダーを呼び出します。プロバイダーはステータスを配信ログに報告します。

14 個のコンポーネント · 14 個の接続

Archboard で開く
ダイアグラムの内容
  • Product services
  • Notification API
  • Preferences
  • Push queue
  • Email queue
  • SMS queue
  • Push workers
  • Email workers
  • SMS workers
  • APNs and FCM
  • Email provider
  • SMS provider
  • Delivery log
  • event
  • opt-outs, quiet hours
  • status
  • webhooks
3 つのチャネルの前に 1 つの API を置いた通知システム。API が重複を排除し、設定を確認し、テンプレートを描画します。その後は各チャネルが専用のキュー、ワーカー、プロバイダーを持ち、配信ステータスは 1 つのログに戻ってきます。

図の読み方は次のとおりです。

  • 左: プロダクトサービスは、冪等キー付きのイベントを通知 API に送ります。API は設定ストアを読み、Markdown ブロックに書いたチェックを実行します。
  • 中央: 各チャネルが専用のキューとワーカープールを持つので、遅いプロバイダーが詰まらせるのは自分のチャネルだけです。
  • 右: ワーカーは APNs と FCM、メールプロバイダー、SMS プロバイダーを呼び出します。ステータスの更新はあとから非同期に届くので、配信ログへは点線のエッジで戻ります。

図に載せるべきこと

  • チェックとその順序。 重複排除、設定、上限、描画が、このシステムの実際のロジックです。
  • チャネルごとのレーン。 図の上ですべてのチャネルが 1 つのキューを共有しているなら、本番でもおそらく共有しています。
  • 再試行とデッドレターの挙動。 各ワーカーに、バックオフと試行回数の上限を書いておきます。
  • ステータスが戻る経路。 これがないと、届かなかったメッセージを誰もデバッグできません。

キューとコンシューマー側について詳しくは、イベント駆動アーキテクチャの記事 を参照してください。このような共有サービスがほかのサービスの間のどこに位置するかは マイクロサービスのアーキテクチャ図 で示しています。また、連絡先や配信ログを保存する前に GDPR のデータマッピングの記事 を読んでおく価値があります。

Archboard で作る

  1. Google または GitHub でサインインし、図を作成します。
  2. C4 モデルパックから、プロダクトサービス用と通知 API 用に コンテナ を 1 つずつ追加し、API の下に設定用の データベース を置きます。
  3. データパックから、チャネルごとに 1 つずつ キュー ノードを 3 つ追加し、各チャネルのワーカー用に コンテナ を追加します。
  4. プロバイダー用に C4 モデルパックから 外部システム ノードを 3 つ、配信ログ用に データベース を追加します。
  5. ステータスのエッジは点線にして、送信前のチェックを書いた基本パックの Markdown ブロックを追加します。

この例で使うダイアグラムノードは 14 個で、Free プランの範囲に収まります。

よくある質問

各サービスがそれぞれ通知を送るのではだめですか? そうすると、設定、おやすみ時間、重複排除、配信追跡をすべてのサービスで作ることになり、少しずつ食い違っていきます。1 つの通知サービスなら、どこでも同じように適用できます。

通知の重複を防ぐには? すべてのリクエストに冪等キーを必須にし、しばらく保存しておきます。同じキーが来たら受理したうえで捨てます。

プッシュ、メール、SMS で 1 つのキューを共有すべきですか? いいえ。キューとワーカープールを分けておけば、遅いプロバイダーや障害の起きたプロバイダーがほかのチャネルを遅らせることはありません。

おやすみ時間はどう扱えばよいですか? ユーザーごとのタイムゾーンとおやすみの時間帯を保存し、急ぎでないメッセージは送ったり捨てたりせず、時間帯の終わりに送るよう予約します。

通知が配信されたことはどうやってわかりますか? プロバイダーの応答と Webhook を、通知 ID をキーにした配信ログに記録します。「配信済み」の意味はチャネルごとに違うので、プロバイダー自身のステータス名を記録します。

無料で始める — 1 シート、クレジットカード不要。

関連記事

← ブログに戻る

1分でキャンバスを開く

Google または GitHub でサインイン。1シートは無料で、チームが加わったらアップグレード。