ストーリー
要件の整理
interface NotificationSystemRequirements {
channels: {
push: "iOS/Android プッシュ通知 (APNs/FCM)";
email: "Eメール通知";
sms: "SMSテキスト通知";
inApp: "アプリ内通知";
webhook: "外部サービスへのWebhook";
};
nonFunctional: {
deliveryRate: "> 99.9%(通知ロスの許容範囲)";
latency: "リアルタイム通知は5秒以内に配信";
scale: "1日10億通知";
dedup: "同一通知の重複配信を防止";
rateLimit: "ユーザーあたりの通知頻度を制御";
preference: "ユーザーの通知設定を尊重";
};
}
ハイレベル設計
┌────────────────────────────────────────────────────────┐
│ │
│ [各サービス] ──→ [通知API] ──→ [メッセージキュー] │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ [バリデーション] [重複排除] [レート制限] │
│ │ │ │ │
│ └───────────┼───────────┘ │
│ ▼ │
│ [ルーティング] │
│ ┌───────┬───┴───┬────────┐ │
│ ▼ ▼ ▼ ▼ │
│ [Push] [Email] [SMS] [InApp] │
│ Worker Worker Worker Worker │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ [APNs] [SES] [Twilio] [WebSocket]│
│ [FCM] │
│ │
│ [通知ログDB] [ユーザー設定DB] [分析サービス] │
│ │
└────────────────────────────────────────────────────────┘
詳細設計
通知パイプライン
// 通知処理のパイプライン
class NotificationPipeline {
async process(notification: NotificationRequest): Promise<void> {
// 1. バリデーション
this.validate(notification);
// 2. ユーザー設定の確認
const preferences = await this.preferenceService
.getPreferences(notification.userId);
if (!preferences.isEnabled(notification.type, notification.channel)) {
return; // ユーザーがオプトアウトしている
}
// 3. 重複排除(Idempotency Key)
const isDuplicate = await this.deduplication
.check(notification.idempotencyKey);
if (isDuplicate) {
return; // 既に処理済み
}
// 4. レート制限
const isRateLimited = await this.rateLimiter
.check(notification.userId, notification.type);
if (isRateLimited) {
// キューに入れて後で再試行
await this.delayQueue.enqueue(notification, { delay: '1h' });
return;
}
// 5. テンプレートのレンダリング
const rendered = await this.templateEngine
.render(notification.templateId, notification.data);
// 6. チャネル別のワーカーに送信
await this.channelRouter.route(notification.channel, {
...notification,
content: rendered,
});
// 7. 通知ログの記録
await this.notificationLog.record(notification);
}
}
配信保証とリトライ
// 指数バックオフ付きリトライ
class NotificationWorker {
private readonly MAX_RETRIES = 3;
private readonly RETRY_DELAYS = [1000, 5000, 30000]; // 1s, 5s, 30s
async deliver(notification: RenderedNotification): Promise<void> {
for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
try {
await this.send(notification);
await this.updateStatus(notification.id, 'delivered');
return;
} catch (error) {
if (attempt === this.MAX_RETRIES) {
await this.updateStatus(notification.id, 'failed');
await this.deadLetterQueue.enqueue(notification);
await this.alertService.notify(`通知配信失敗: ${notification.id}`);
return;
}
await this.sleep(this.RETRY_DELAYS[attempt]);
}
}
}
// チャネル別の配信処理
private async send(notification: RenderedNotification): Promise<void> {
switch (notification.channel) {
case 'push':
await this.pushService.send(notification);
break;
case 'email':
await this.emailService.send(notification);
break;
case 'sms':
await this.smsService.send(notification);
break;
}
}
}
通知のグルーピングとダイジェスト
// 大量通知のグルーピング
class NotificationAggregator {
// 「Aさん、Bさん、他3人があなたの投稿にいいねしました」
async aggregate(userId: string, type: string): Promise<AggregatedNotification> {
const pending = await this.getPendingNotifications(userId, type);
if (pending.length <= 1) {
return this.createSingleNotification(pending[0]);
}
// グルーピング
return {
type: 'aggregated',
summary: `${pending[0].actorName}、${pending[1].actorName}、他${pending.length - 2}人があなたの投稿にいいねしました`,
count: pending.length,
items: pending.slice(0, 5), // 最新5件の詳細
};
}
}
トレードオフの整理
| 判断ポイント | 選択肢 | 推奨理由 |
|---|---|---|
| キューイング | Kafka vs SQS | Kafka: 高スループット、順序保証 |
| テンプレート | サーバーレンダリング vs クライアント | サーバー: 一貫性、チャネル横断 |
| 重複排除 | Redis Set vs DB | Redis: 高速チェック、TTL自動削除 |
| レート制限 | Token Bucket vs Sliding Window | Sliding Window: 公平性が高い |
まとめ
| ポイント | 内容 |
|---|---|
| マルチチャネル | Push/Email/SMS/InAppを統一パイプラインで処理 |
| 配信保証 | 指数バックオフリトライ + デッドレターキュー |
| ユーザー体験 | 通知設定、レート制限、グルーピングで制御 |
| 冪等性 | Idempotency Keyで重複配信を防止 |
チェックリスト
- マルチチャネル通知のアーキテクチャを設計できた
- 配信保証のリトライ戦略を理解した
- 重複排除とレート制限の実装方法を把握した
- 通知グルーピングの設計を理解した
次のステップへ
次は演習です。Step 2で学んだケーススタディの知識を活用して、自分でシステムを設計してみましょう。
推定読了時間: 30分