第07章:Outboxパターンの全体像(登場人物紹介)🗺️👥
今日のゴール 🎯✨
この章の終わりに、あなたがこう言えたら勝ちです👇
- 「Outboxって、**“発送待ち箱”**をDBに作るやつだよね📦」
- 「同じトランザクションで“業務テーブル更新”と“Outboxに積む”をやるんだよね🔒」
- 「送るのは後で。送る係(Relay)が配達するんだよね🚚📩」
7.1 Outboxパターンって、ざっくり何?🐣📦
Outboxパターンはひとことで言うと…
**「DB更新」と「外部への通知(メッセージ送信)」がズレないように、 まず“送る内容”をDBのOutboxテーブルに保存して、後から確実に配達する仕組み」**🛡️
この考え方は、分散システムで“確実にイベントを届けたい”ときの定番として整理されています。(Microsoft Learn)
7.2 登場人物(この章の主役たち)👥✨
① 業務テーブル(例:Orders)🧾🛒
ふつうのアプリが持つ「本命のデータ」だよ〜
- 注文、会員、支払い、在庫…みたいなやつ!
② Outboxテーブル(発送待ち箱)📦📮
「あとで送る用の封筒」を入れておく箱。 ここに入るのは、だいたいこんな情報👇
- OutboxId(箱の整理番号)🪪
- Type(何の通知?)🏷️
- Payload(中身:JSONなど)🧠
- OccurredAt(いつ起きた?)⏰
- Status(未送信/送信済/失敗…)✅⚠️
③ Relay(配送係 / 配達ロボ)🚚🤖
Outboxを定期的に見に行って、未送信を配達する係。 実装としては Worker / BackgroundService がすごく相性いいよ〜🧑💻 (Hosted Serviceでバックグラウンド処理を書くのが定番、って公式でも説明されてるよ)(Microsoft Learn)
④ 送信先(外の世界)🌍📩
- メッセージブローカー(キュー/トピック)📬
- HTTP API 呼び出し🌐
- メール送信サービス✉️ …などなど。「アプリの外」だから失敗しやすい子たち😅
7.3 全体の流れ(ストーリーで覚える)📚✨

✅ ステップ0:事件は「業務処理」から起きる
例)「注文が作られた!」🛒✨
✅ ステップ1:同じトランザクションで “2つ” 書く 🔒🍙
- Orders に注文を保存🧾
- Outbox に「注文作成イベント」を積む📦
ここがOutboxの心臓だよ🫀 「片方だけ成功」が起きにくくなるのがポイント!
✅ ステップ2:Relayが後でOutboxを見に行く 👀⏱️
未送信(Pending)を探して…
✅ ステップ3:外へ配達する 🚚📩
送信できたら…
✅ ステップ4:Outboxを“送信済”にする ✅
(失敗したら、失敗として記録してリトライへ…は後の章で!🧯)
7.4 文字で見る「超ミニ図解」🖼️➡️🧠
(同じDBトランザクション)
[OrdersにINSERT/UPDATE] + [OutboxにINSERT(Pending)] ✅
(後で別プロセス/別スレッド)
RelayがOutbox(Pending)を取得 → 外部へ送信 → OutboxをSentに更新 ✅
7.5 最小のテーブル案(雰囲気つかむ用)🧱📦
※あとで「設計章」でガッツリやるけど、今は“雰囲気”だけ!
CREATE TABLE OutboxMessages (
Id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
Type NVARCHAR(200) NOT NULL,
Payload NVARCHAR(MAX) NOT NULL,
OccurredAt DATETIME2 NOT NULL,
Status INT NOT NULL, -- 0:Pending, 1:Sent, 2:Failed など
SentAt DATETIME2 NULL,
Error NVARCHAR(MAX) NULL
);
CREATE INDEX IX_OutboxMessages_Status_OccurredAt
ON OutboxMessages (Status, OccurredAt);
7.6 「同じトランザクションで2つ書く」って、C#だとどんな感じ?🔒✍️
例:注文作成+Outbox積み(概念サンプル)🛒📦
(EF Core 10は .NET 10 とセットで動くよ、って公式に書かれてるよ)(Microsoft Learn) (.NET 10 は 2025-11-11 リリースのLTSだよ)(Microsoft)
public async Task<Guid> CreateOrderAsync(CreateOrderCommand cmd, CancellationToken ct)
{
var orderId = Guid.NewGuid();
var outboxId = Guid.NewGuid();
await using var tx = await _db.Database.BeginTransactionAsync(ct);
// ① 業務テーブル更新(本命)
_db.Orders.Add(new Order
{
Id = orderId,
CustomerId = cmd.CustomerId,
TotalPrice = cmd.TotalPrice,
CreatedAt = DateTimeOffset.UtcNow
});
// ② Outboxに「送る内容」を積む(発送待ち箱)
_db.OutboxMessages.Add(new OutboxMessage
{
Id = outboxId,
Type = "OrderCreated.v1",
Payload = System.Text.Json.JsonSerializer.Serialize(new
{
OrderId = orderId,
cmd.CustomerId,
cmd.TotalPrice
}),
OccurredAt = DateTimeOffset.UtcNow,
Status = OutboxStatus.Pending
});
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
return orderId;
}
ここでの大事ポイントはこれ👇✨
- Orders と Outbox を “同じ tx” で確定してる🔒
- だから「注文は保存できたけど通知は消えた😱」が起きにくい!
7.7 Relay(配送係)の“頭の中”🧠🚚
Relayはだいたいこんなことをぐるぐるしてます👇
- Pendingを一定件数だけ取る📦
- 送る📩(HTTP/キューなど)
- 成功したら Sent にする✅
- 失敗したら Failed にして理由を残す⚠️(→次でリトライ)
Hosted Service / BackgroundService でバックグラウンド処理を書くのが王道だよ〜(Microsoft Learn) それを Windows Serviceとして動かすやり方も公式でまとまってるよ🪟(Microsoft Learn)
7.8 よくある勘違い(ここで潰しとこ!)🧨😺
❌ 勘違い1:「Outboxはメッセージを直接“送る仕組み”でしょ?」
👉 ちがうよ〜! Outboxは **“送るための記録を残す箱”**📦 送るのはRelay(配送係)🚚
❌ 勘違い2:「業務DBとは別DBにOutboxを置いてもいい?」
👉 初心者のうちは 同じDB が安心! 「同じトランザクション」が効かなくなると、ズレ防止の力が弱まっちゃう😭
❌ 勘違い3:「Relayは1台だけ動く前提でいいよね?」
👉 うっかり二重起動👯すると、同じPendingを2回触ることがあるよ〜 (このへんは後の章で“重複・冪等性”につながるよ🧷)
7.9 ミニ演習(5分)⏱️🧪
演習A:Outboxが“発送待ち箱”っぽいのを確認📦👀
- 注文作成を1回実行🛒
- DBで OutboxMessages を見てみる👀
- Pending が増えてたらOK✅
演習B:外部送信が落ちても“記録が残る”のを想像💭🧯
- Relayの送信部分だけ例外を投げる(throw)ようにしてみて、 Outboxに Failed と Error が残るイメージを持とう⚠️ (実装は後の章で丁寧にやるよ)
7.10 Copilot / Codex に頼むときの“良い聞き方”🤖💡
そのままコピペで使えるやつ置いとくね👇✨
- 「EF Coreで OutboxMessages テーブル用の Entity と DbContext 設定例を書いて」🧩
- 「BackgroundServiceで“5秒ごとにPendingを10件取得して送る”雛形を書いて」⏱️🚚
- 「SQL Serverで Pendingを取り出すクエリ例(ロック競合が少ないやつ)を提案して」🔍
- 「送信成功したOutboxをSentに更新する処理の例外パターンを列挙して」🧯
※ただし、トランザクション範囲と二重起動対策は人間が最終チェックね👀✅
まとめ(この章で覚える合言葉)🔑✨
- Outbox=発送待ち箱📦
- 同じトランザクションで 業務更新+Outbox積み🔒
- 送るのは後で、Relay(配送係) がやる🚚📩
- ここから先で「テーブル設計」「Relay実装」「冪等性」「リトライ」に育てていくよ🌱