第93章:イベントの持つ情報:入れすぎ注意📦⚖️

この章のテーマはひとことで言うと… **「イベントは“通知”であって“全部入りレポート”にしない」**だよ〜🔔✨ (イベントが太ると、後々しんどくなる率がめちゃ高い…!😵💫)
1) まず結論:イベントは「薄く」するのが基本 🥗✨
イベント設計には「薄いイベント(Thin)」と「厚いイベント(Thick / Fat)」って考え方があるよ〜📮 厚いイベントは「必要そうな情報を全部詰めて送る」やり方。薄いイベントは「起きた事実+最小限の識別情報だけ送る」やり方。
そして実務では、薄いイベントが“筋肉”💪になりやすい(疎結合を保ちやすい)って話がよく出るよ〜。(Thoughtworks)
2) 「入れすぎ」が何を壊すの?(ありがち事故)💥😇
イベントが太ると、だいたいこのへんが壊れるよ〜👇
- 密結合になる:購読側が「このフィールドある前提」になって、発行側が変えられなくなる🔗😵
- 変更が地獄:フィールド追加・削除が“契約変更”になって、互換性が怖い🧨
- 情報漏えいリスク:個人情報とか内部データが混ざりやすい🙅♀️🔐
- サイズ増で遅くなる:配信コスト・保存コスト・ログも重くなる🐘
- “どれが正”か分からなくなる:イベント内の値が「計算済みの表示用」だったりして、真実がブレる😵💫
3) そもそも:ドメインイベントは「内側の合図」📣🏠
ここ、超大事ポイント!💡 ドメインイベントは「同じ境界の中(同じドメインの内側)で、何かが起きたよ〜」って伝える合図。 外のシステムに公開する“契約”にするのは別物で、そっちはよく「統合イベント(Integration Event)」として分けて扱うよ〜🌍📨 (Microsoft for Developers)
なので基本方針はこう👇
- 🏠 ドメインイベント:内部用。ドメインの言葉でOK(ただし太らせない)
- 🌍 統合イベント:外部向け。互換性・バージョン・個人情報・契約を超意識(さらに薄くしがち)
(このロードマップは戦術DDD中心なので、まずは内部のドメインイベントを上手に扱うのが主役だよ〜🎀)
4) “イベントの情報”を2種類に分けよう:メタ情報とデータ 🧾🧩
イベントって、実務ではよく「封筒(エンベロープ)」+「中身(データ)」の2階建てで考えるよ〜📩✨
A. メタ情報(追跡のための情報)🧭
たとえば CloudEvents って標準仕様だと、イベントには id / source / type / specversion みたいな“最低限の文脈”を必須にしてるよ〜。(GitHub)
さらに time(いつ起きたか)は任意属性として定義されてる。(cloudevents.github.io)
DDDの内部イベントでCloudEventsをそのまま使う必要はないけど、**「最低限のメタ情報を持つ発想」**はめちゃ参考になるよ〜📌✨
DDD的におすすめのメタ情報セット(例)👇
eventId:イベントの一意ID(重複処理対策にも効く)🆔type:イベント種類(例:PaymentCompleted)🏷️occurredAt:発生時刻⏰aggregateId:どの集約の話?🪪aggregateVersion:集約の何番目の変化?(あれば強い)🔢correlationId/causationId:追跡したいなら(最初は任意でOK)🧵
B. データ(“起きた事実”そのもの)📦
ここが今日の主役! データは「起きた事実」を表すのに必要最小限にするよ〜⚖️✨
5) ルール:イベントに「入れてOK / 基本NG」早見 🧁📌
✅ 入れてOK(入れる価値が高い)✨
- 識別子(
orderIdなど)🪪 - その瞬間の“事実として残したい値”(支払金額、通貨、確定した割引額など)💴
- 購読側が“どうしてもその時点で必要”で、後から取りに行けない情報(ただし慎重に)🧐
❌ 基本NG(入れると後で泣く)😭
- 集約を丸ごと(Order全部)🏯🚫
- 表示用に整形した文字列(
"支払い完了です"みたいな)🖼️🚫 - いつでも計算できる派生値(合計・ポイント残高など)➕🚫
- DB行っぽいデータ(内部カラム、フラグ乱舞)🧱🚫
- 個人情報(メール・住所など)は“最小限”でも慎重に🔐🚫
6) 例題:カフェ注文で「太いイベント」→「薄いイベント」へ 🥤➡️🥗

😇 悪い例:全部盛りイベント(やりがち)
「購読側が欲しそうだから」って、Orderの中身を丸ごと入れちゃうパターン💥
// ❌ 太い(Thick)イベント例:Orderの中身を詰め込みすぎ
type PaymentCompleted_Fat = {
eventId: string;
type: "PaymentCompleted";
occurredAt: string;
// ぜんぶ入り…😵💫
order: {
id: string;
status: "Paid";
items: Array<{ menuId: string; name: string; price: number; qty: number }>;
totalPrice: number;
customer: { id: string; name: string; email: string };
// ...将来どんどん増える
};
};
これ、最初は便利に見えるんだけど… Order構造を変えた瞬間にイベント契約が壊れるし、購読者が増えるほど地獄になるよ〜😇🧨
🥰 良い例:薄いイベント(おすすめ)
「起きた事実」と「識別子」を中心にする✨
// ✅ 薄い(Thin)イベント例:必要最小限
type PaymentCompleted = {
eventId: string;
type: "PaymentCompleted";
occurredAt: string;
aggregate: {
name: "Order";
id: string; // orderId
version: number; // あれば強い(任意でもOK)
};
data: {
paymentId: string; // 支払いの同一性
amount: number; // “事実として残したい値”
currency: "JPY"; // 例題なので固定でもOK
};
};
ポイントはこれ👇
- Orderの構造を送ってない(購読側は必要なら取得する)
- イベントは「支払い完了」という事実に集中してる📣✨
amountみたいな“その時点の確定値”は入れてOK(後から変えない前提)💴
7) 「購読側が欲しい情報」はどうするの?🔎📚
答え:購読側で取りに行くことが多いよ〜🏃♀️💨 (だから“薄いイベント”が成立する✨)
たとえば「レシート作成」側は、イベントを受け取ったら…
async function onPaymentCompleted(ev: PaymentCompleted) {
// 1) orderIdで集約を取得
const order = await orderRepository.findById(ev.aggregate.id);
// 2) 必要な情報を使って副作用(レシート作成など)
const receipt = Receipt.issueFrom(order, ev.data.paymentId, ev.data.amount);
// 3) 保存・通知など
await receiptRepository.save(receipt);
}
この形にしておくと、Orderの内部構造が変わっても イベント契約が壊れにくいんだ〜🥰🔧
8) でも薄すぎると困らない?(トレードオフ)⚖️🤔
薄いイベントにも弱点はあるよ〜👇
- 取得先が落ちてたら処理できない(同期購読だと特に)😵
- 取得時点で状態が変わってるかも(整合性の話)⏳
だから実務では、だいたいこうバランス取るよ〜🍰
- 購読の目的が“監査・記録”なら、事実として残す値はイベントに入れる🧾✨
- 購読の目的が“何か作業する”なら、識別子+最小の確定値(不足分は取得)🔎
- 「後から絶対再現したい」なら、イベントソーシングやスナップショットの世界(別の設計になる)📚
9) “重複配信”が起きる世界なので、IDは超だいじ 🔁🛡️
メッセージングは「少なくとも1回配信(at-least-once)」が多くて、同じイベントが2回届くことがあるよ〜😇 だから購読側は「同じイベントを2回処理しても壊れない(冪等)」が基本ルール!(microservices.io)
そのためにも eventId(一意ID)をメタ情報として持っておくのが強いよ〜🆔✨
さらに、追跡ID(tracking ID)をイベントに入れて重複検知する、という実務パターンもよく紹介されるよ〜。(Confluent)
(このロードマップだと冪等性は後の章でガッツリやるけど、第93章の時点でも “IDは太らせずに守備力を上げる” って覚え方が最高だよ🛡️💛)
10) 実装テンプレ:TypeScriptで「太らない」イベント型を作る 🧩✨
ベース型(おすすめ)
type ISODateTime = string;
type DomainEvent<TType extends string, TData> = Readonly<{
eventId: string;
type: TType;
occurredAt: ISODateTime;
aggregate: Readonly<{
name: string; // "Order" など
id: string; // AggregateId
version?: number; // あれば
}>;
data: Readonly<TData>;
}>;
具体イベント
type PaymentCompleted = DomainEvent<
"PaymentCompleted",
Readonly<{
paymentId: string;
amount: number;
currency: "JPY";
}>
>;
Readonly を強めに使うと、イベントが“改ざんされない事実”っぽくなって気持ちいいよ〜🧊✨
11) テスト:イベントが太らないように“見張る”👀🧪
型だけでもかなり守れるけど、最低限これもやると強いよ〜💪
✅ 1) シリアライズサイズを雑チェック(太り検知)
import { test, expect } from "vitest";
test("PaymentCompleted event should be small", () => {
const ev = {
eventId: "e-001",
type: "PaymentCompleted",
occurredAt: "2026-02-07T00:00:00Z",
aggregate: { name: "Order", id: "o-001", version: 12 },
data: { paymentId: "p-999", amount: 1200, currency: "JPY" },
} satisfies PaymentCompleted;
const bytes = Buffer.byteLength(JSON.stringify(ev), "utf8");
expect(bytes).toBeLessThan(800); // 目安:適当に始めてOK👍
});
✅ 2) dataのキー数を固定(増えたら気づける)
test("PaymentCompleted.data keys should stay minimal", () => {
const data = { paymentId: "p-999", amount: 1200, currency: "JPY" };
expect(Object.keys(data).sort()).toEqual(["amount", "currency", "paymentId"]);
});
こういうテストがあると、誰かが「つい便利だから追加😇」しても、すぐ止まるよ〜🚦✨
12) AIの使い方(第93章向けプロンプト集)🤖💞
コピペして使えるやつ置いとくね〜🧁
🧠 イベント項目の棚卸し
- 「このイベントの購読者(利用者)を想定して、必須情報 / あれば便利 / いらないに分類して。理由も短く。」
🧹 入れすぎ検知
- 「次のイベントpayloadを見て、密結合・個人情報・派生値・過剰な構造の観点で危険箇所を指摘して。削った最小案も出して。」
🧪 テスト追加
- 「イベントが肥大化しないためのテスト戦略を、TypeScript + Vitestで提案して。サイズ、キー固定、バージョン互換の観点も入れて。」
章末ミニ演習 🎓✨
🧁 お題
OrderPlaced(注文作成)イベントを作ってみよう!
✅ やること
- まず“太い版”をわざと作る(Order丸ごと)😇
- そこから「本当に必要?」って質問しながら削る✂️
- 最後に「薄い版」にして、テスト(キー固定 or サイズ)を追加🧪✨
理解チェック(サクッと5問)📝💡
- イベントに「集約を丸ごと入れる」と何が起きやすい?😵💫
- “事実として残す値”と“派生値”の違いって?📌
eventIdを持つと何が嬉しい?🆔- 薄いイベントの弱点は?どうバランス取る?⚖️
- 「ドメインイベント」と「統合イベント」を分ける理由は?🏠🌍
次の第94章では、いよいよ イベント購読(同期でまず簡単に)🔔 を実装して、「イベントが役に立つ瞬間」を体験していくよ〜🎉✨