第55章:トランザクション感覚(超入門)🧾⏱️
- TypeScriptで“雰囲気トランザクション”を疑似実装して、動きで理解する🎮✨
1) この章のゴール🎯✨
- 「トランザクション」= 原子性(いっこにまとめる) の感覚がつく🧠⚡
- 「集約」と「トランザクション境界」がどう関係するか分かる🏯🧾
- 「注文確定」のユースケースで、どこまでを“一気にやる”か決められる✅
- TypeScriptで“雰囲気トランザクション”を疑似実装して、動きで理解する🎮✨
2) トランザクションってなに?☕🧾(超やさしく)
トランザクションは一言でいうと👇
「まとめてやって、全部うまくいったら確定!途中でコケたら全部なかったことにする!」 (All or Nothing)🎲💥➡️✅ or ❌
DBの世界ではこれを ACID(Atomicity / Consistency / Isolation / Durability)って呼ぶよ📚✨ 特にこの章で大事なのは Atomicity(原子性)=「まとめて成功/失敗」!🧾✨ (PostgreSQL)
3) DDDだと何が嬉しいの?🏯🛡️
✅ 集約は「ルールを守るまとまり」
集約は「この範囲の整合性(不変条件)は絶対守る!」っていう城🏯🛡️だったよね。
✅ トランザクションは「成功/失敗のまとまり」
だから基本はこう考えると超ラク👇
- 集約のルールを守る操作は、1回のトランザクションに閉じる🔒✨
- 集約をまたぐ“つながり”は、あとで イベント/非同期/最終的整合性 で扱う(後半でやるやつ)📮⏳
「集約=整合性の境界」 「トランザクション=成功/失敗の境界」 → だいたい重ねると強い💪💕
4) 例題:『注文確定』はどこまで“一気にやる”?☕✅🧾
あなたのロードマップにある通り、この章のテーマは👇
「注文確定」はどこまで一気にやる?🤔
たとえば ConfirmOrder(注文確定)でやりたいことを並べると…
✅ 同じ集約(Order集約)の中で完結するもの(=一気にやりやすい)🏯✨
- 注文の状態を
Draft → Confirmedにする🚦✅ - 明細が空なら確定できない、などの不変条件チェック🔒
- 合計金額の整合性を保つ(明細合計と一致)💴🧾
👉 これは Order集約の城の中 の話だから、基本「1トランザクションで一気に」がおすすめ💡
⚠️ 別集約・外部連携が絡むもの(=一気にやると重くなりがち)🌍⚡
- 決済サービスへ連携💳
- 在庫集約の更新📦
- 通知メール送信✉️
👉 ここまでを“全部いっぺんに”やると、失敗時の巻き戻しが地獄になりがち😵💫 このへんは後半(イベントやOutbox等)で強くするのがDDDの定番ルートだよ📮✨
5) トランザクション境界の決め方(ミニ指針)🧭✨
迷ったらこの3つだけ見よっ👇🥰
-
不変条件(絶対守るルール)を守るのに同時更新が必要? 🔒
-
途中で失敗したら“なかったこと”にしたい? ❌➡️🧾
-
外部I/O(決済・通知)を巻き込んでない? 🌍
- 巻き込むほど「長いトランザクション」になって事故りやすい💥
6) TypeScriptで“疑似トランザクション”してみる🎮🧾✨
まだDBなしでも、雰囲気は作れるよ! ここでは「失敗したら保存結果を戻す」を InMemoryで疑似的に再現して、感覚をつかむ💡
6-1) 最小のインターフェース(TransactionRunner)🏃♀️💨
export interface TransactionRunner {
run<T>(fn: () => Promise<T>): Promise<T>;
}
6-2) リポジトリ(最低限)📚
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
6-3) ドメイン(超簡略)🏯🚦
type OrderStatus = "Draft" | "Confirmed" | "Paid" | "Cancelled";
export class Order {
private constructor(
public readonly id: string,
private status: OrderStatus,
private readonly items: ReadonlyArray<{ sku: string; qty: number }>
) {}
static create(id: string) {
return new Order(id, "Draft", []);
}
addItem(sku: string, qty: number) {
if (this.status !== "Draft") throw new Error("確定後は明細を変更できないよ😢");
if (qty <= 0) throw new Error("数量は1以上だよ😢");
return new Order(this.id, this.status, [...this.items, { sku, qty }]);
}
confirm() {
if (this.status !== "Draft") throw new Error("Draftのときだけ確定できるよ😢");
if (this.items.length === 0) throw new Error("明細が空だと確定できないよ😢");
return new Order(this.id, "Confirmed", this.items);
}
snapshot() {
// 永続化用にシリアライズ可能な形へ(超簡略)
return { id: this.id, status: this.status, items: this.items };
}
static restore(s: { id: string; status: OrderStatus; items: ReadonlyArray<{ sku: string; qty: number }> }) {
return new Order(s.id, s.status, s.items);
}
}
6-4) ユースケース(ここでトランザクション境界を作る)🎬🧾
export class ConfirmOrderUseCase {
constructor(
private readonly tx: TransactionRunner,
private readonly orders: OrderRepository
) {}
async execute(orderId: string): Promise<void> {
await this.tx.run(async () => {
const order = await this.orders.findById(orderId);
if (!order) throw new Error("注文が見つからないよ😢");
const confirmed = order.confirm(); // ルールはドメインが守る🏯🛡️
await this.orders.save(confirmed); // 保存はまとめて確定したい🧾✨
});
}
}
7) 疑似TxのInMemory実装(失敗したらロールバックっぽく)🔁🧯
ポイントはこれ👇 run()の中で失敗したら、Mapを元に戻す(超ミニRollback)✨
export class InMemoryOrderRepository implements OrderRepository {
private store = new Map<string, ReturnType<Order["snapshot"]>>();
async findById(id: string): Promise<Order | null> {
const s = this.store.get(id);
return s ? Order.restore(s) : null;
}
async save(order: Order): Promise<void> {
this.store.set(order.id, order.snapshot());
}
// テスト用
seed(order: Order) {
this.store.set(order.id, order.snapshot());
}
// トランザクション用にスナップショット
cloneStore(): Map<string, ReturnType<Order["snapshot"]>> {
return new Map(this.store);
}
restoreStore(snapshot: Map<string, ReturnType<Order["snapshot"]>>) {
this.store = snapshot;
}
}
export class InMemoryTransactionRunner implements TransactionRunner {
constructor(private readonly repo: InMemoryOrderRepository) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
const before = this.repo.cloneStore();
try {
const result = await fn();
return result; // commit(何もしない)
} catch (e) {
this.repo.restoreStore(before); // rollbackっぽい!
throw e;
}
}
}
8) テストで「失敗したら戻る」を体験🧪✨(Vitest想定)
import { describe, it, expect } from "vitest";
describe("ConfirmOrderUseCase", () => {
it("明細が空なら確定できず、状態も変わらない(rollback)", async () => {
const repo = new InMemoryOrderRepository();
const tx = new InMemoryTransactionRunner(repo);
const usecase = new ConfirmOrderUseCase(tx, repo);
const order = Order.create("o-1");
repo.seed(order);
await expect(usecase.execute("o-1")).rejects.toThrow();
const reloaded = await repo.findById("o-1");
expect(reloaded?.snapshot().status).toBe("Draft"); // Confirmedになってない!✨
});
it("明細があれば確定できる(commit)", async () => {
const repo = new InMemoryOrderRepository();
const tx = new InMemoryTransactionRunner(repo);
const usecase = new ConfirmOrderUseCase(tx, repo);
const order = Order.create("o-2").addItem("latte", 1);
repo.seed(order);
await usecase.execute("o-2");
const reloaded = await repo.findById("o-2");
expect(reloaded?.snapshot().status).toBe("Confirmed");
});
});
「成功/失敗をまとめる」ってこういうことか〜!ってなるはず🥰🎉
9) 実DBだとどう書くの?(雰囲気だけ)💾🧾
たとえば Prisma なら $transaction で「関数の中をひとまとめ」にできるよ〜✨
エラーが起きたらロールバック、が基本の挙動だよ🧯🔁 (Prisma)
await prisma.$transaction(async (tx) => {
// tx を使ってDB操作をまとめる(イメージ)
// const order = await tx.order.findUnique(...)
// await tx.order.update(...)
});
10) 今どきのTS/Nodeの“前提ライン”(2026/02の目安)🧠✨
- TypeScript公式の「最新版」は 5.9 と案内されてるよ📌 (TypeScript)
- Node.jsは「偶数版がLTS」運用で、v24がActive LTS、v25がCurrent(2026/02時点)になってるよ📌 (Node.js)
(※将来のTypeScriptの“ネイティブ版”開発も進んでるけど、この章の設計思想自体は変わらないよ〜🚀) (Microsoft Developer)
11) AIの使いどころ(この章向け)🤖💬✨
🧠 ① トランザクション境界の壁打ち
「このユースケースはどこまでを1回でやるべき?」を整理したいとき👇
あなたはDDDの設計レビュアです。
題材:カフェ注文ドメイン
ユースケース:注文確定(ConfirmOrder)
やりたい処理:
- 注文をDraft→Confirmed
- 明細が空なら失敗
- (候補)在庫を減らす
- (候補)支払い連携
- (候補)通知メール
質問:
1) どれを1トランザクションに入れるべき?
2) 集約境界(Order/Stock/Payment)を仮定して、理由も説明して
3) “長いトランザクション”のリスクを、この例で具体化して
出力は箇条書きで。
🧪 ② 失敗パターン洗い出し(ロールバック観点)
ConfirmOrderで起きうる失敗を10個出して。
「失敗したら何を元に戻すべきか」もセットで。
(例:状態、明細、合計、外部通知の有無など)
12) まとめ🎒✨(この章で一番大事な一言)
集約のルールを守る操作は、トランザクションで“まとめて確定”する🏯🧾✨ これだけ覚えたら、次の実装(Order集約をガチで固める)にめちゃ効くよ〜!🛡️🔥
次章(第56章)では、いよいよ Order集約を“安全な城”として実装していくよ🏯💪✨