第26章:冪等性(Idempotency)入門🔁✨ 〜「リトライされても壊れない状態機械」へ〜
この章はね、「同じイベントが2回以上来ても、結果が“1回分”になる」状態機械を作れるようになる回だよ〜😊💖 (通信って…普通にリトライされるので…対策しないと“二重注文”“二重課金”が起きがち😵💫💥)
1) この章でできるようになること🎯✨
- 「冪等性」って言葉を 状態機械の文脈で説明できる📚😊
- 二重クリック・タイムアウトリトライ・Webhook重複…みたいな 現実の事故を想定できる👆⏳📨
- イベント(Command)に IdempotencyKey(冪等キー) を持たせて、重複を安全に吸収できる🔑✨
- 「重複を捨てる」だけじゃなく、前回と同じ結果を返す設計ができる✅🔁
2) まず冪等性ってなに?🧠✨

超ざっくり言うと…
同じ操作を何回やっても、最終的な結果が変わらないこと🔁✨
HTTPの公式定義(超要約すると)も、「同じリクエストを複数回送っても、意図する効果が1回と同じなら冪等」って言ってるよ📮✨ (RFC Editor)
HTTPでイメージする小ネタ🌐
- GET / PUT / DELETE は冪等(仕様でそう扱う)
- POST は冪等じゃないことが多い(“作成”が多いから) (RFC Editor)
でもこの章は「HTTPの話」より、状態機械での冪等性が主役だよ🍙📱✨
3) なぜ状態機械で冪等性が必要?😵💫💥(事故あるある)
「学食モバイル注文」で想像してみよ〜🍙📱
事故あるある①:支払いボタン連打👆👆👆
- ユーザーが「反応ない…?」って思って2回押す
- Payイベントが2回飛ぶ
- 対策ないと → 二重課金💸💸(最悪)
事故あるある②:通信タイムアウトで自動リトライ⏳🔁
- サーバーは処理したけど、返事が届かなかった
- クライアントは「失敗した!」と思って同じ操作を再送
- 対策ないと → 同じ注文を2回進める😇
だから「重複を捌く or 処理を冪等に」が必要、っていう話がよく出てくるよ📨🔁 (Microsoft Learn)
4) 状態機械での冪等性のゴール🏁✨
ゴールはこれ👇
✅ 同じIdempotencyKeyのイベントがもう一回来ても
- 状態は進まない(副作用も増えない)
- 前回と同じ結果(成功/失敗)を返す
「重複は無視!」だけだと、クライアントが「え、成功したの?失敗したの?」って迷子になりがちなので、結果も再利用するのが強いよ〜🔁✨
Stripeみたいな決済系も「同じキーなら安全にリトライできる」思想で、冪等キーを使う設計を推してるよ💳🔑 (Stripe ドキュメント)
5) 実装方針(レベル別)📶✨
Lv1:操作を“自然に冪等”にする(最小)🌱
例:
-
「Paidにする」イベントが来た
- まだPaidじゃない → Paidへ
- もうPaid → 何もしない(結果だけ返す)
ただし!⚠️ 状態は変わらなくても、メール送信やレシート発行みたいな副作用が2回走るとアウトなので、結局Lv2が欲しくなること多いよ😵💫💥
Lv2:IdempotencyKeyで重複排除+結果再利用(おすすめ)🥇
- イベントに IdempotencyKey を付ける🔑
- サーバー側で「このキー処理済み?」を記録🗃️
- 処理済みなら 前回の結果を返す🔁✨
Lv3:Inbox/Outboxやメッセージングまで含めて堅牢に(発展)🚀
- DBに「受信イベント(Inbox)」を残して重複排除
- 「送信イベント(Outbox)」で副作用も1回に このへんは後ろの章の“永続化”とも相性がいいよ💾✨
6) IdempotencyKey設計のコツ🔑✨(ここが勝負!)
✅ ルール1:リトライ時は“同じキー”を使う
- 「送信し直すたびに新しいGUID」だと、重複排除できないよ😇💦
✅ ルール2:キーの単位を決める(何に対して一意?)
おすすめはだいたい👇
- 「注文ID × 操作種別(Pay/Cancelなど) × クライアントが生成したキー」
✅ ルール3:同じキーで“内容が違う”のはエラーにする
例:
- 同じキーなのに金額が違う → 危険すぎなので弾く🚫💥 (安全のため「キー + リクエストのハッシュ」を保存して照合することが多いよ)
例えばStripeは「キーは一定時間でシステムから消える」運用を明示してるよ🕒 (Stripe ドキュメント)
7) ハンズオン🛠️✨:状態機械に冪等レイヤーを足す
ここでは Consoleでも動くミニ実装でいくね😊 (後でAPIにしても考え方は同じ!)
7-1. まずはCommandにキーを持たせる📦🔑
public interface ICommand
{
string IdempotencyKey { get; }
}
public sealed record PayCommand(decimal Amount, string IdempotencyKey) : ICommand;
public sealed record CancelCommand(string Reason, string IdempotencyKey) : ICommand;
- IdempotencyKeyは Guid文字列にすると楽(例:Guid.NewGuid().ToString("N"))✨
7-2. 状態と結果の型を用意する📘✨
public enum OrderState
{
Draft,
Submitted,
Paid,
Cancelled
}
public enum ApplyStatus
{
Applied, // 遷移した
Rejected, // 禁止遷移などで失敗
DuplicateReplay // 同じキーなので前回結果を返した
}
public sealed record ApplyResult(
ApplyStatus Status,
OrderState Before,
OrderState After,
string Message
);
7-3. 「処理済みキー」を保存するストア🗃️✨(今回はメモリ版)
using System.Collections.Concurrent;
public interface IIdempotencyStore
{
bool TryGet(string key, out ApplyResult result);
bool TryPut(string key, ApplyResult result);
}
public sealed class MemoryIdempotencyStore : IIdempotencyStore
{
private readonly ConcurrentDictionary<string, ApplyResult> _map = new();
public bool TryGet(string key, out ApplyResult result)
=> _map.TryGetValue(key, out result!);
public bool TryPut(string key, ApplyResult result)
=> _map.TryAdd(key, result);
}
7-4. 状態機械の適用処理に「冪等チェック」を入れる🔁🔑
public sealed class Order
{
public string OrderId { get; }
public OrderState State { get; private set; }
public Order(string orderId, OrderState state = OrderState.Draft)
{
OrderId = orderId;
State = state;
}
public void SetState(OrderState next) => State = next;
}
public sealed class OrderStateMachine
{
private readonly IIdempotencyStore _store;
public OrderStateMachine(IIdempotencyStore store)
{
_store = store;
}
public ApplyResult Apply(Order order, ICommand command)
{
// ① まず「同じキー処理済み?」を確認
if (_store.TryGet(command.IdempotencyKey, out var cached))
{
return cached with { Status = ApplyStatus.DuplicateReplay };
}
// ② 未処理なら通常処理して結果を作る
var before = order.State;
ApplyResult result = command switch
{
PayCommand pay => ApplyPay(order, pay),
CancelCommand cancel => ApplyCancel(order, cancel),
_ => new ApplyResult(ApplyStatus.Rejected, before, before, "未対応のコマンドだよ🥺")
};
// ③ 結果を保存(次回以降のリプレイ用)
_store.TryPut(command.IdempotencyKey, result);
return result;
}
private static ApplyResult ApplyPay(Order order, PayCommand cmd)
{
var before = order.State;
if (before is OrderState.Submitted)
{
order.SetState(OrderState.Paid);
return new ApplyResult(ApplyStatus.Applied, before, order.State, $"支払いOK💳✨ 金額={cmd.Amount}");
}
return new ApplyResult(ApplyStatus.Rejected, before, before, "今は支払いできない状態だよ🚫");
}
private static ApplyResult ApplyCancel(Order order, CancelCommand cmd)
{
var before = order.State;
if (before is OrderState.Draft or OrderState.Submitted)
{
order.SetState(OrderState.Cancelled);
return new ApplyResult(ApplyStatus.Applied, before, order.State, $"キャンセルOK🙆♀️ 理由={cmd.Reason}");
}
return new ApplyResult(ApplyStatus.Rejected, before, before, "今はキャンセルできない状態だよ🚫");
}
}
✅ これで「同じキーで来たら前回結果を返す」になるよ🔁✨ (成功も失敗も再利用するのがポイント!)
8) 動作チェック(ミニ実験)🧪✨
✅ 実験1:Payを2回送る(同じキー)
1回目:Applied(Submitted → Paid) 2回目:DuplicateReplay(状態はそのまま、結果だけ返る)🔁✨
✅ 実験2:Payを2回送る(違うキー)
1回目:Applied 2回目:Rejected(もうPaidだから)🚫
この違いで、「二重クリックは同じキーで吸収」「別操作として来たら普通に判定」ができるよ😊
9) 実務で必ず出る“もう一段むずい話”😈(でも大事!)
9-1. 同時に2個来たら?(in-flight重複)⚡
- リトライが「前の処理が終わる前」に届くことある その場合は、
- ストアに「処理中」マーカーを入れる
- あるいはDBの一意制約で“先着1名だけ”通す みたいな対策が必要になるよ💥
9-2. 冪等性と並行制御は別モノだよ🧩
- 冪等性:同じ操作の重複を安全に
- 並行制御:別操作が同時に走るのを安全に
DB使うなら、EF Coreの楽な手として 楽観的同時実行(Concurrency Token) があるよ🧷✨ (Microsoft Learn)
10) 演習(この章のゴール演習)📝✨
演習A:IdempotencyKeyを「注文ID + 操作種別」でスコープ分けしてみよ🔑
- CancelとPayでキーがぶつからないようにする (例:OrderId + ":PAY:" + Key)
演習B:同じキーで「金額が違う」場合は弾く🚫
- ストアに「Amountも一緒に保存」して照合 (本番だと “リクエストのハッシュ” が多いよ)
演習C:テストを書く🧪
- 同じキー2回 → 2回目はDuplicateReplay
- 違うキー2回 → 2回目はRejected
11) AIの使いどころ🤖✨(Copilot / Codex向け)
そのままコピペで使える指示例💬
- 「OrderStateMachineに冪等ストアを追加して、同じキーなら前回結果を返す実装にして」
- 「同じキーで内容が違うケース(Amount違い)を検出する設計案を3つ出して」
- 「xUnitで、同一キーの2回適用テストと、別キーの2回適用テストを書いて」
AIに書かせたら、最後にこれだけは自分でチェックしてね✅
- “同じキー”のとき 副作用が増えないか?(ログ/通知/DB更新など)
- “失敗結果”も 同じ結果が返るか?
12) まとめ📌💖
- 冪等性は「リトライされる世界」で生きるための防具🛡️✨
- 状態機械では 同じイベントが複数回来る前提で作るのが強い🔁
- IdempotencyKey + 結果保存で 二重課金・二重注文・二重処理を止められる💳🚫✨
- 並行制御(同時更新)とは別なので、必要なら別対策も足す🧩
おまけ:最新版メモ🗒️✨(リサーチ結果)
- .NET 10 は 2025-11-11 リリースのLTSとして案内されてるよ📌 (Microsoft)
- C# 14 は Visual Studio 2026 / .NET 10 SDK で試せる新機能として整理されてるよ✨ (Microsoft Learn)
次の第27章は「時間で動く(Timeout・期限)」だよ⏰✨ 冪等性を入れた状態機械に、期限切れとか自動キャンセルを足して“さらに現実っぽく”していこ〜😊💖