第95章:非同期の考え方(なぜ必要?)⏳🌍

この章のゴール🎯💕
この章を読み終わったら、こんな状態になれます👇✨
- 「同期でやるべき処理」と「非同期に逃がすべき処理」の見分けがつく👀✅
- 非同期にすると 何がラクになって、何が難しくなるか が言葉で説明できる🗣️🌼
- 次章(冪等性🔁🛡️)に向けて「重複・順序・再試行」の地雷を先に知っておける💣➡️🧯
1) まず同期のおさらい🔔☕
第94章はこんな感じでしたよね👇 「ドメインイベントが起きた → その場で購読者が処理する」方式✨
イメージ(同期)📌
- 例:支払い完了イベント → その場でレシート作成 → その場で通知
- つまり「今この瞬間に全部やる」💪🔥
同期の強み💎
- 結果がすぐ揃う(画面に即反映しやすい)✨
- 失敗したらその場で止められる(分かりやすい)🧯
同期の弱み😵💫
- 1つ重い処理があると 全体が遅くなる 🐢💦
- 外部APIが不調だと 全体が巻き添えで落ちる 🌩️
- 一時的に注文が殺到すると 詰まって崩れやすい 🚧💥
2) 非同期が必要になる“よくある瞬間”⏳🌍
DDDのイベント連携で非同期が欲しくなるのは、だいたいこの3つ👇💡
① 重い処理を混ぜたくない🏋️♀️💦
- PDFレシート生成📄
- 画像変換🖼️
- 集計・分析📊
- まとめてメール送信✉️ → これ、支払い処理の中でやると遅いです😇
② 外部連携は“失敗する前提”🌩️🔌
決済・通知・配送・CRM… 外は落ちるし遅いしタイムアウトします(現実)🥲 だから 「外がコケても注文の核心は守る」 方向に寄せたい✨
③ “混雑”を吸収したい🚶♀️🚶♀️🚶♀️➡️🧺
注文が一気に増える日(セール、TVで紹介、昼休み)🍔🔥 同期だとサーバが「一気に全部やって」パンクしがち💥 非同期だと「いったんキューに積んで、順番にさばく」ができる🧺✅
3) 非同期にすると“世界がどう変わるか”🌈
非同期にすると、こういう感じになります👇
同期(その場で全部)⚡
- 画面で「支払い完了」→ すぐレシートも通知も全部終わってる✨
非同期(あとでやる)⏳
- 画面で「支払い完了」→ 支払いだけ確定 ✅
- レシート作成や通知は あとで勝手に進む 🚶♀️💨
- だから一瞬だけ「反映待ち」みたいなズレが起きることがある🌫️ これが 最終的整合性(eventual consistency) の入口です🌍✨ (“いつか揃うけど、今この瞬間は揃ってないことがある”)(ウィキペディア)
4) 非同期の基本セット🧰✨(超ざっくり版)

非同期の設計って、道具は増えるけど、考え方は単純です💕
📮 キュー(Queue)
「あとでやる仕事の箱」🧺 入れる人=Producer(発行側) 取り出す人=Consumer(処理側)
👷 ワーカー(Worker)
キューから仕事を取り出して実行する子✨
- 失敗したらリトライするかも🔁
- めちゃ失敗するなら隔離(DLQ)するかも🪦
🔁 配送保証(超重要)
現実のメッセージングはだいたい “少なくとも1回”(重複あり得る)です😇 例:Amazon SQSのStandard Queueは at-least-once で、重複を想定して冪等にせよ、が基本方針になってます(AWS ドキュメント) (Azure Service Busも「at-least-once」やDLQなどの信頼性機能を前提に語られます)(Microsoft Learn)
👉 つまり:同じイベントが2回届いても壊れない設計が必要 → 次章(第96章:冪等性🔁🛡️)へ繋がる✨
5) まずは“疑似キュー”で体験しよう🎮☕
いきなり本物のMQ(Kafkaとか)に行くと怖いので、まずは アプリ内の簡易キューで感覚を掴みます🧺✨ (本番では耐久性や再起動耐性が必要になるけど、ここでは理解優先!)
5.1 ドメインイベント(最小)📣
export type DomainEvent = Readonly<{
type: string;
occurredAt: Date;
payload: unknown;
}>;
export const PaymentCompleted = (args: {
orderId: string;
amountYen: number;
}): DomainEvent => ({
type: "payment.completed",
occurredAt: new Date(),
payload: { ...args },
});
5.2 同期購読(第94章っぽい)🔔
「publishした瞬間にhandlerを呼ぶ」⚡
type Handler = (event: DomainEvent) => void | Promise<void>;
export class SyncEventBus {
private handlers = new Map<string, Handler[]>();
on(type: string, handler: Handler) {
const list = this.handlers.get(type) ?? [];
list.push(handler);
this.handlers.set(type, list);
}
async publish(event: DomainEvent) {
const list = this.handlers.get(event.type) ?? [];
for (const h of list) {
await h(event); // ここで重い処理があると詰まる😵💫
}
}
}
5.3 非同期:publishは“積むだけ”🧺✨
「publishは軽く終える」→ 仕事は後でワーカーが処理👷
type Handler = (event: DomainEvent) => void | Promise<void>;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export class InProcessAsyncEventBus {
private handlers = new Map<string, Handler[]>();
private queue: DomainEvent[] = [];
private running = false;
on(type: string, handler: Handler) {
const list = this.handlers.get(type) ?? [];
list.push(handler);
this.handlers.set(type, list);
}
// ✅ publishは“積むだけ”なので速い
publish(event: DomainEvent) {
this.queue.push(event);
}
// 👷 ワーカー起動(アプリ起動時に1回呼ぶイメージ)
startWorker() {
if (this.running) return;
this.running = true;
(async () => {
while (this.running) {
const event = this.queue.shift();
if (!event) {
await sleep(10);
continue;
}
const list = this.handlers.get(event.type) ?? [];
for (const h of list) {
try {
await h(event);
} catch (e) {
// ここでは雑にログだけ(本番はリトライやDLQが欲しい)🧯
console.error("async handler failed", event.type, e);
}
}
}
})();
}
stopWorker() {
this.running = false;
}
}
5.4 体験:支払いは即完了、レシートは後で☕📄
const bus = new InProcessAsyncEventBus();
bus.startWorker();
bus.on("payment.completed", async (e) => {
// 重い処理のつもり😇
await new Promise((r) => setTimeout(r, 500));
console.log("✅ receipt created:", e.payload);
});
// 支払い完了の瞬間に全部終わらせない✨
bus.publish(PaymentCompleted({ orderId: "O-001", amountYen: 1200 }));
console.log("✅ payment finished (fast!)");
ポイント💡
- 「支払い完了」のログはすぐ出る✅
- 「レシート作成」は0.5秒後に出る📄⏳ → “あとでやる”が体感できる🎉
6) 非同期で増える“困りごと”トップ3🥲⚠️
非同期は万能じゃなくて、代わりにこの3つが増えます👇
① 重複(同じイベントが2回来る)🔁😇
多くの仕組みは at-least-once なので、重複しうるのが前提です(AWS ドキュメント) → だから次章の「冪等性」が必須になる🛡️✨
② 順序(1→2→3の順で来ないかも)🔀
- 「支払い完了」より先に「レシート発行」処理が走ったら事故💥
- なので “同じ注文IDは順番を守る” 工夫が必要
例:
- Azure Service Bus なら Sessions で順序処理を作れる話があります(Microsoft Learn)
- Google Cloud Pub/Sub も ordering key などで順序を扱いつつ、再配送で後続も巻き戻る注意が書かれています(Google Cloud)
③ 失敗の扱い(リトライ・隔離・見える化)🧯👀
非同期は「失敗しても後で取り返せる」が強み✨ でも設計しないと「失敗が闇に消える」😱 だから
- リトライ回数🔁
- 失敗の隔離(DLQ)🪦
- 監視(ログ・メトリクス)📈 がセットになる(Azure Service BusもDLQなど信頼性機能に触れます)(Microsoft Learn)
7) どこまで非同期にする?判断のコツ🧠✨
迷ったらこの基準でOKです👇🌼
同期が向いてる✅
- その場で「成功/失敗」を返さないとUXが崩れる(支払い確定など)💳
- 強い整合性が必須(在庫の確保を同時に確定したい、など)🔒
非同期が向いてる✅
- 重い・遅い・外部(通知、レシート生成、分析、検索インデックス更新)📩📄📊
- 一時的な混雑を吸収したい(注文殺到)🧺
- 失敗しても「後からやり直せばOK」な仕事🔁✨
8) AIに手伝ってもらうと速いところ🤖💞
🧠 設計の壁打ちプロンプト例
- 「同期購読でやってる“レシート作成”を非同期に分けたい。副作用を分離して、必要なインターフェース案と責務を箇条書きにして」
- 「“重複配送”が起きたときに壊れない設計チェックリストを作って」
- 「注文ID単位で順序を守る案を3つ。実装コストと運用コストも比較して」
🧪 テスト観点を増やすプロンプト例
- 「非同期化した後の失敗パターン(タイムアウト、二重実行、順序逆転)をGiven/When/Thenで10個出して」
9) ミニ演習(やってみよ〜!)🧁✍️
演習1:レシート作成を“非同期”に移す📄➡️🧺
PaymentCompletedを publish する- レシート作成は handler に寄せる
- publish 側(支払いユースケース)は“速く終わる”状態にする
✅ ゴール:支払い処理が軽くなることをログで確認🎉
演習2:失敗をわざと起こす🌩️
- handler 内でランダムに例外を投げる🎲
- ログに「失敗したイベント」を出す🧯 ✅ ゴール:「失敗が見える」状態を作る👀
(次章で、ここに“冪等キー”や“重複防止”を足して強くするよ🔁🛡️)
10) ふりかえりクイズ🎓💗
- 非同期にすると「速くなる」代わりに増える問題を3つ言える?(ヒント:重複・順序・失敗)🔁🔀🧯
- 「今すぐ揃ってないかも」を何て呼ぶ?🌍
- at-least-once ってどういう意味?(そして何が必要になる?)🛡️
おまけ:2026年2月時点の“周辺の最新”メモ🗓️✨
- TypeScript 5.9 は公式発表があり(2025/08/01)、GitHubのリリース上でも 5.9.x が最新として見えます(Microsoft for Developers)
- TypeScript のネイティブ移植プレビュー(npm+VS Code向け)も公式に案内されています(Microsoft for Developers)
- 進捗アップデート(TypeScript 7関連)も公式ブログで継続的に出ています(Microsoft for Developers)
- Node.js は 2026/02/03 に v25.6.0(Current)がリリースされています(nodejs.org)
次の第96章は、この章で出てきた最大の地雷💣 「同じイベントが2回来ても安全(冪等)」 をガッツリやります🔁🛡️✨