マルチチャネル通知システム:テンプレート、再試行、ユーザー設定
メール、SMS、Telegram 向けに、テンプレート、配信ステータス、再試行、ユーザー設定を一貫して扱えるマルチチャネル通知システムを設計する。

単一の通知システムで解決できること
メール、SMS、Telegram を別々の機能として作ると、すぐに問題が出ます。同じアラートでも文言やタイミング、誰に届くかのルールがバラバラになりがちです。サポートチームはメールプロバイダ、SMS ゲートウェイ、ボットのログと、三つの "真実" を追いかけることになります。
マルチチャネル通知システムは、通知を三つの統合ではなく一つのプロダクトとして扱うことでこれを解決します。1つのイベントが発生(パスワードリセット、請求書の支払い、サーバーダウンなど)し、システムがテンプレート、ユーザー設定、配信ルールに基づいてチャネル横断でどう届けるかを決定します。チャネルごとにフォーマットは変えられますが、意味やデータ、追跡は一貫します。
ほとんどのチームが必要とする基盤は同じです:変数を持つバージョン管理されたテンプレート、配信ステータス追跡("送信済み、配達済み、失敗、理由")、妥当な再試行とフォールバック、同意とクワイエットアワーを含むユーザー設定、そしてサポートが推測することなく確認できる監査証跡。
成功は良い意味で地味です。メッセージは予測可能になり、正しい人に正しい内容が適切なタイミングで、許可されたチャネルを通じて届きます。問題が起きても、すべての試行が明確なステータスと理由コードと共に記録されているのでトラブルシュートは簡単です。
「新しいログイン」アラートは良い例です。1回作成して同じユーザー・デバイス・場所データを使い、詳細はメール、緊急性はSMS、素早い確認はTelegramで届けます。SMS プロバイダがタイムアウトしたら、システムは予定どおり再試行し、タイムアウトをログに残し、アラートを破棄する代わりに別チャネルにフォールバックできます。
コア概念とシンプルなデータモデル
通知の「なぜ」を「どう届けるか」から分離すると管理しやすくなります。つまり、共有オブジェクトを少数にし、チャネル固有の詳細は本当に違うときだけ持つということです。
まず イベント から始めます。イベントは order_shipped や password_reset のような名前付きトリガーです。名前は一貫しておきましょう:小文字、アンダースコア、場合によって過去形を使う。イベントをテンプレートや設定ルールが依存する安定した契約として扱います。
1つのイベントから 通知(notification) レコードを作成します。これはユーザー向けの意図:誰に、何が起きたか、コンテンツをレンダリングするために必要なデータ(注文番号、配達日、リセットコード)を表します。ここに user_id、event_name、locale、priority、scheduled_at のような共通フィールドを保存します。
次にチャネルごとに メッセージ に分割します。通知は 0 ~ 3 件のメッセージ(メール、SMS、Telegram)を作るかもしれません。メッセージには宛先(メールアドレス、電話番号、Telegram chat_id)、template_id、レンダリング済みのコンテンツ(メールの件名/本文、SMS の短文)などチャネル固有のフィールドを持たせます。
最後に各送信を 配信試行(delivery attempt) として追跡します。試行にはプロバイダの request_id、タイムスタンプ、レスポンスコード、正規化されたステータスを含めます。ユーザーが「届いていない」と言ったときに確認するのはこれです。
シンプルなモデルは通常4つのテーブルまたはコレクションに収まります:
- Event(許可されたイベント名とデフォルトのカタログ)
- Notification(ユーザーの意図ごとに1つ)
- Message(チャネルごとに1つ)
- DeliveryAttempt(各試行ごとに1つ)
冪等性(idempotency)を早めに計画してください。各通知に (event_name, user_id, external_ref) のような決定論的キーを付け、上流システムからの再実行で重複が発生しないようにします。ワークフローのステップが再実行されたとき、冪等キーがなければユーザーに2通のSMSを送ってしまいます。
監査に必要なものだけを長期保存し、短期の配信キューや生のプロバイダペイロードは運用とトラブルシュートに必要な期間だけ保持しましょう。
実用的なエンドツーエンドフロー(ステップバイステップ)
「何を送るかを決める」処理と「送る」処理を分けると、アプリは高速になり障害の扱いも簡単になります。
実用的なフローは次のとおりです:
-
イベントプロデューサーが通知リクエストを作る。 例:
password reset、invoice paid、ticket updated。リクエストには user ID、メッセージ種別、コンテキストデータ(注文番号、金額、担当者名)を含めます。監査のためにリクエストはすぐ保存します。 -
ルーターがユーザーとメッセージルールを読み込む。 ユーザーの許可チャネル、オプトイン、クワイエットアワーや、たとえばセキュリティアラートはメールを優先する、のようなメッセージルールを見ます。ルーターはチャネル計画を決定します(例:Telegram → SMS → メール)。
-
システムはチャネルごとの送信ジョブをエンキューする。 各ジョブはテンプレートキー、チャネル、変数を含みます。ジョブはキューへ入れ、ユーザー操作を送信でブロックしないようにします。
-
チャネルワーカーがプロバイダ経由で配信する。 メールは SMTP かメール API、SMS は SMS ゲートウェイ、Telegram はボット経由。ワーカーは冪等に作り、同じジョブを再試行しても重複送信が発生しないようにします。
-
ステータス更新は一箇所に戻る。 ワーカーは queued、sent、failed、可能なら delivered を記録します。プロバイダが "accepted" のみを返す場合はそれも記録し、delivered と区別して扱います。
-
フォールバックと再試行は同じ状態から動く。 Telegram が失敗したら、ルーター(または再試行ワーカー)が SMS を次にスケジュールして文脈を失わないようにします。
例:ユーザーがパスワードを変更した場合。バックエンドはユーザーと IP アドレスを含む単一のリクエストを出します。ルーターはユーザーが Telegram を優先しているがクワイエットアワーで夜間はブロックされるのを見て、メールは今、Telegram は朝にスケジュールし、両方を同じ通知レコードで追跡します。
AppMaster を使って実装する場合は、リクエスト、ジョブ、ステータステーブルを Data Designer に保持し、ルーティングや再試行ロジックを Business Process Editor で表現し、送信は非同期で行って UI の応答性を保つとよいでしょう。
チャネル横断で機能するテンプレート構造
良いテンプレートシステムは一つの考えから始まります:あなたはイベントについて通知しているのであって「メールを送っている」のではない、ということ。イベントごとに1つのテンプレートを作り(Password reset、Order shipped、Payment failed など)、その下にチャネル別のバリアントを保存します。
全チャネルで同じ変数名を使い続けてください。メールが first_name と order_id を使うなら、SMS や Telegram も同じ名前を使うべきです。そうすることで、あるチャネルでレンダリングが空欄になるような微妙なバグを防げます。
繰り返し使えるシンプルなテンプレート形
各イベントごとに、チャネルごとに小さなフィールドセットを定義します:
- Email: subject、preheader(オプション)、HTML 本文、テキストフォールバック
- SMS: プレーンテキスト本文
- Telegram: プレーンテキスト本文、オプションのボタンや短いメタデータ
チャネルごとに変わるのはフォーマットだけで、意味は変えないでください。
SMS は短さのため特別ルールが要ります。文字数制限を決め、長すぎる場合の処理(切って ... をつける、オプション行を先に削るなど)を一貫して決めましょう。長いURLや余計な句読点を避け、重要なアクション(コードや期限、次のステップ)を先に置きます。
ビジネスロジックをコピーせずにロケール対応する
言語はパラメータとして扱い、別ワークフローにしないでください。イベントとチャネルごとに翻訳を保存し、同じ変数でレンダリングします。Order shipped のロジックは同じで、件名や本文だけロケールごとに変わります。
プレビュー機能は費用対効果が高いです。サンプルデータ(長い名前のエッジケースを含む)でテンプレートをレンダリングし、サポートがメール、SMS、Telegram のバリアントを公開前に確認できるようにしましょう。
信頼できてデバッグしやすい配信ステータス
後から「それに何が起きたか」を答えられることが通知の価値です。通知内容と各配信試行を分離して扱いましょう。
まずはメール、SMS、Telegram 全てで同じ意味を持つ小さな共通ステータスを用意します:
- queued: システムに受け入れられ、ワーカー待ち
- sending: 配信試行中
- sent: プロバイダAPIへ正常に渡した
- failed: アクションを取れるエラーで終了
- delivered: ユーザーに到達した証拠がある(可能な場合)
これらをメインのメッセージレコードに持たせ、すべての試行を履歴テーブルで追跡します。履歴があることでデバッグが簡単になります:試行1はタイムアウトで失敗、試行2は成功、あるいは SMS は成功したがメールはバウンスし続けた、など。
試行ごとに保存するもの
プロバイダの表現を正規化して保存し、違うプロバイダが別の言葉を使っても検索や分類ができるようにします。
- provider_name と provider_message_id
- response_code(TIMEOUT、INVALID_NUMBER、BOUNCED 等の正規化コード)
- raw_provider_code と raw_error_text(サポート用)
- started_at、finished_at、duration_ms
- channel(email、sms、telegram)と destination(マスク済み)
部分的成功を想定してください。1つの通知が親IDとビジネス文脈(order_id、ticket_id、alert_type)を共有する3つのメッセージを作る場合、SMS は送信済みでメールが失敗した、という全体の話を1箇所で見たいはずです。
「配達済み(delivered)」の実態
「送信済み(sent)」は「配達済み(delivered)」ではありません。Telegram では API が受け入れた情報しか得られないことがあり、SMS やメールの配達は webhook やプロバイダのコールバックに依存します。すべてのプロバイダが同じ信頼性ではありません。
チャネルごとの delivered の定義を前もって決めておきましょう。Webhook 確認がある場合はそれを使い、ない場合は delivered を不明として sent を報告する方が正直でサポートの回答も一貫します。
再試行、フォールバック、停止の条件
再試行は通知システムでよく失敗する部分です。速すぎる再試行は嵐を作り、永遠に再試行すると重複やサポートの手間が増えます。目的は単純:成功する見込みがあるなら再試行し、見込みがないなら停止する。
まず失敗を分類します。メールプロバイダのタイムアウト、SMS ゲートウェイの 502、Telegram API の一時エラーは通常再試行可能です。一方、 malformed なメールアドレス、検証に失敗する電話番号、ボットにブロックされた Telegram チャットは再試行しても無駄です。これらを区別しないとコストを浪費しログを埋めます。
実用的な再試行プランは上限がありバックオフを使います:
- 試行1:今すぐ送信
- 試行2:30秒後
- 試行3:2分後
- 試行4:10分後
- アラートなら最大経過時間(例:30〜60分)で停止
停止はデータモデルに明確に位置づけましょう。再試行上限を超えたらメッセージをデッドレター(failed-permanently)としてマークします。最後のエラーコードと短いエラーメッセージを保持し、サポートが推測せずに対応できるようにします。
成功後の重複送信を防ぐには冪等性を使います。論理メッセージごとに冪等キーを作成(多くは notification_id + user_id + channel)。プロバイダが遅れて応答し再試行した場合、2回目を重複と認識してスキップできるようにします。
フォールバックは「パニックで自動的に行う」ものではなく意図的に設計するべきです。重要度と時間に基づくエスカレーションルールを定義してください。例:パスワードリセットはプライバシー上の理由で別チャネルへのフォールバックを行うべきではないが、運用インシデントアラートは Telegram が2回失敗したら SMS を試し、10分後にメールを試す、など。
ユーザー設定、同意、クワイエットアワー
システムが「賢い」と感じられるのはユーザーを尊重する時です。最も簡単な方法は、通知タイプごとにチャネルをユーザーが選べるようにすることです。多くのチームは通知タイプを security、account、product、marketing のように分類します。法律やルールがタイプごとに異なるためです。
チャネルが使えない場合でも動く簡潔な設定モデルから始めてください。ユーザーにメールはあるが電話番号がない、あるいは Telegram をまだ接続していない、ということは普通に起きます。マルチチャネル通知システムはそれをエラーと扱うべきではありません。
ほとんどのシステムは次のようなコンパクトなフィールドセットを必要とします:通知タイプ(security、marketing、billing)、タイプごとの許可チャネル(email、SMS、Telegram)、チャネルごとの同意(日時、ソース、必要なら証明)、チャネルごとのオプトアウト理由(ユーザーの選択、バウンス、"STOP" 返信 など)、およびクワイエットアワールール(開始/終了とユーザーのタイムゾーン)。
クワイエットアワーでよく壊れるポイントはタイムゾーンです。オフセットだけでなくユーザーのタイムゾーンを保存し、サマータイムの変化で驚かないようにしましょう。クワイエットアワー中にメッセージがスケジュールされたら失敗させずに deferred とマークし、次に許可された送信時刻を選んでください。
デフォルトは重要です。よくある方針は:セキュリティ通知はクワイエットアワーを無視(ただし法的に必要なハードオプトアウトは尊重)し、非重要な更新はクワイエットアワーとチャネル選択に従う、というものです。
例:パスワードリセットは最速の許可されたチャネルへ即時に送信する。週次ダイジェストは朝まで待ち、ユーザーが明示的に有効化していない限り SMS をスキップする。
運用:モニタリング、ログ、サポートワークフロー
通知がメール、SMS、Telegram をまたぐ時、サポートは素早く答えを出す必要があります:送ったか、届いたか、何が失敗したか。マルチチャネル通知システムは、裏で複数のプロバイダを使っていても、調査が一箇所で完結するように感じられるべきです。
まずは誰でも使えるシンプルな管理ビューを作りましょう。ユーザー、イベント種別、ステータス、時間窓で検索でき、最新の試行が上に来るようにします。各行はチャネル、プロバイダ応答、次の予定アクション(再試行、フォールバック、停止)を表示します。
問題を早く捕まえるための指標
障害は一つの明確なエラーとして現れることは稀です。少数の指標を追い、定期的にレビューしてください:
- チャネルごとの送信率(毎分メッセージ数)
- プロバイダ別・エラーコード別の失敗率
- 再試行率(何件が再試行を必要としたか)
- 配達時間(queued から delivered、p50 と p95)
- ドロップ率(ユーザー設定、同意不足、最大再試行超過で停止した割合)
すべてを相関させましょう。イベント発生時に相関IDを生成し(例:"invoice overdue")、テンプレート、キュー、プロバイダ呼び出し、ステータス更新へ渡します。ログではそのIDが1つのスレッドになり、複数チャネルへファンアウトしたイベントを追う際に役立ちます。
サポート向けの安全なリプレイ
リプレイは必要ですが、ガードレールが無いとスパムや二重課金を招きます。安全なリプレイの流れは通常:特定のメッセージIDのみを再送、送る前にテンプレートバージョンとレンダリング済みの内容を表示、理由と実行者を必須入力で保存、既に配達済みなら明示的に強制しない限りブロック、ユーザーとチャネルごとのレート制限を課す、などです。
通知のセキュリティとプライバシーの基本
マルチチャネル通知システムは個人データ(メール、電話番号、チャットID)に触れ、ログインや支払いなどセンシティブな場面を扱います。すべてのメッセージ本文とログ行は後で見られる可能性があると想定し、保存と閲覧を制限する設計をしてください。
テンプレートにセンシティブなデータを入れないようにしましょう。テンプレートは再利用できて平凡なものに:"Your code is {{code}}" のようなテンプレートは問題ありませんが、アカウントの詳細や長いトークンなど乗っ取りに使える情報は避けます。ワンタイムコードやリセットトークンが必要なら、生の値ではなく検証に必要なハッシュと有効期限だけを保存します。
通知イベントを保存・ログに残す際は積極的にマスクしてください。サポート担当はコードが送付されたことを知る必要はあっても、コード自体を知る必要は通常ありません。電話番号やメールも配達に必要なフル値は保持しておきつつ、ほとんどの画面ではマスクした表示にします。
ほとんどの事故を防ぐ最低限のコントロール
- ロールベースのアクセス:メッセージ本文や受信者の完全情報を見られるのは限られたロールだけにする。
- デバッグアクセスとサポートアクセスを分離し、トラブルシュートがプライバシー漏洩にならないようにする。
- Webhook エンドポイントを保護:署名済みコールバックや共有シークレットを使い、タイムスタンプを検証し不明な送信元は拒否する。
- 機密フィールドは静止時に暗号化、転送時は TLS を使用する。
- 保持ルールを定義:詳細ログは短期間だけ保持し、その後は集計やハッシュ化した識別子だけを残す。
実用例:パスワードリセットの SMS が失敗して Telegram にフォールバックする場合、試行、プロバイダステータス、マスク済み受信者は記録しますが、リセットリンク自体を DB やログに保存しないようにします。
例シナリオ:1つのアラート、3チャネル、実際の結果
顧客の Maya は Password reset と New login の2つの通知タイプを有効にしており、Telegram を第一、次にメールを優先します。Password reset では SMS をフォールバックとしてのみ許可しています。
ある夜、Maya がパスワードリセットを要求します。システムは安定した ID を持つ単一の通知レコードを作り、彼女の現在の設定に基づいてチャネル試行に展開します。
Maya が見るものはシンプルです:数秒で Telegram メッセージが届き、短いリセットコードと有効期限が表示されます。他は届きません。Telegram が成功しフォールバックは不要だったためです。
システムが記録するものは詳細です:
- Notification: type=PASSWORD_RESET, user_id=Maya, template_version=v4
- Attempt #1: channel=TELEGRAM, status=SENT then DELIVERED
- メールやSMSの試行は作られない(ポリシー:最初の成功で停止)
週の後半、別のデバイスから New login アラートが出ます。Maya のログインアラート設定は Telegram のみです。Telegram へ送信したところ一時的なエラーが返り、システムはバックオフで2回再試行した後に FAILED とし停止します(このアラートタイプはフォールバック不可)。
ある時、Maya が旅行中に別のパスワードリセットを要求します。Telegram を送ったが、60 秒以内に配達されなければ SMS フォールバックする設定です。SMS プロバイダがタイムアウトし、システムはタイムアウトを記録して一度再試行し、2回目は成功しました。Maya は1分後に SMS のコードを受け取ります。
Maya がサポートに連絡すると、担当はユーザーと時間窓で検索し、試行履歴:タイムスタンプ、プロバイダ応答コード、再試行回数、最終結果を即座に確認できます。
クイックチェックリスト、よくある失敗、次のステップ
マルチチャネル通知システムは次の2つの質問に素早く答えられると運用が楽になります:"私たちは正確に何を試行したか?" と "その後何が起きたか?"。チャネルやイベントを増やす前にこのチェックリストを使ってください。
クイックチェックリスト
- 明確なイベント名とオーナー(例:
invoice.overdueは billing の所有) - テンプレート変数は一度だけ定義(必須/任意、デフォルト、フォーマットルール)
- 事前合意したステータス(created、queued、sent、delivered、failed、suppressed)と各ステータスの意味
- 再試行上限とバックオフ(最大試行回数、間隔、停止ルール)
- 保持ルール(メッセージ本文、プロバイダ応答、ステータス履歴をどれだけ保持するか)
もし1つだけやるなら、sent と delivered の違いをわかりやすく書き留めてください。Sent はシステムが行ったこと、Delivered はプロバイダが報告したことです(遅延や欠落があり得ます)。この2つを混同するとサポートや関係者を混乱させます。
避けるべき一般的なミス
- sent を成功と見なして配信率を過大報告すること
- チャネル別テンプレートが乖離してメール、SMS、Telegram の内容が矛盾するように放置すること
- 冪等性なしに再試行し、プロバイダがタイムアウトした後に受理した場合に重複を発生させること
- 永久に再試行し、一時的な障害を騒がしいインシデントに変えること
- 「念のため」で個人データをログやステータスに過剰に保存すること
まずは1つのイベントと1つの主要チャネルで始め、二つ目のチャネルはフォールバックとして追加してください(並列一斉送信ではなく)。フローが安定したらイベント単位で拡張し、テンプレートと変数を共有してメッセージの一貫性を保ちましょう。
ハンドコーディングを全て行わずに構築したい場合、AppMaster (appmaster.io) はコア部分に実用的な適合を示します:Data Designer でイベント、テンプレート、配信試行をモデル化し、Business Process Editor でルーティングと再試行を実装し、メール・SMS・Telegram を統合しつつステータストラッキングを一元化できます。


