ブログ記事
TypeScriptで学ぶDDDの実装:集約をIDで参照するという設計判断の意味
「ドメイン駆動設計(DDD)は概念が難解で、実際にどうコードに落とすかが分からない」——この壁に当たったことがある開発者は多い。山下祐也・著、増田亨・監修の『つくりながら学ぶ!ドメイン駆動設計 実践入門』は、オンライン書店の開発をTypeScriptで実装するという具体的なコードを通して、その「どうコードに落とすか」に答えを出そうとしている。
1. アーキテクチャの型よりもドメインを先に
クリーンアーキテクチャのフォルダ構成を整えてから開発を始める——この順序に問題がある、と本書は示唆する。
フォルダ構成の整合性はツールや規約が保証できる。しかしビジネスのルール(ドメイン)をどのクラスに持たせるかは、チームがドメインを理解しているかどうかで決まる。本書が最初に扱うのはドメインの定義であり、インフラの設定ではない。
「DDDは手順書ではなく、考え方」という立場が実装例の随所に表れている。
2. 集約を小さく保つ——ID参照という設計判断
DDDを学ぶ開発者が最初に直面する難問の一つが「集約(Aggregate)の境界をどこに引くか」だ。
ORMに慣れた開発者は、関連するオブジェクトをすべてネストして表現しがちだ。商品クラスが注文リストを持ち、注文クラスがレビューを持つ——という構造は直感的だが、問題を抱える。
- 商品を1件読み込む際に、関連する何百件もの注文データが一緒にロードされる
- 別々のユーザーが異なる変更を同じ集約に加えようとすると、トランザクション競合が起きやすくなる
本書の解決策は「他の集約への参照はIDで持つ」というルールだ。Order クラスは Product オブジェクトを直接持たず ProductId だけを持つ。必要な時に必要なデータだけ取得する。
この設計はコーディング規約ではなく、アーキテクチャ上の判断だ。後からマイクロサービスへ分割する際の境界になり、分散システムでの独立したデプロイを可能にする。
flowchart LR
subgraph NG["❌ オブジェクト直参照(巨大な集約)"]
O1["Order"] --> P1["Product(全データ)"] --> R1["Review(全データ)"]
end
subgraph OK["✅ ID参照(境界が明確な集約)"]
O2["Order\n- productId のみ保持"] -.->|"必要時に取得"| P2["Product 集約"]
P2 -.->|"必要時に取得"| R2["Review 集約"]
end
style NG fill:#fce8e8,stroke:#e53935
style OK fill:#e8f8e8,stroke:#4caf50
3. 値オブジェクトで「貧血モデル」を脱する
ゲッターとセッターだけを持つデータの入れ物クラスは「貧血モデル」と呼ばれる。ビジネスロジックを持たないため、利用側があちこちでバリデーションや計算を行うことになる。
値オブジェクトは逆のアプローチをとる。「商品名は1文字以上50文字以下でなければならない」というルールを ProductName クラスのコンストラクタで保証する。TypeScriptの静的型システムを利用して、不正な状態を「型として表現不可能」にする。
class ProductName {
private constructor(private readonly value: string) {}
static create(name: string): ProductName {
if (name.length < 1 || name.length > 50) {
throw new Error('商品名は1〜50文字で入力してください');
}
return new ProductName(name);
}
}
このクラスを string の代わりに引数型として使えば、コンパイル時に渡し間違いを検出できる。
4. 複数集約にまたがる整合性——Outboxパターン
集約を小さく分割すると、複数の集約を一度のトランザクションで更新する「即時整合性」を保ちにくくなる。
例えば、注文確定時に「注文の保存」と「メール通知の送信」の両方が必要な場合、DBへの保存は成功したが通知が失敗するケースへの対応が必要になる。
Outboxパターンはこの問題に対処する設計だ。アプリケーションはDBへの書き込みと同じトランザクション内に「送信すべきイベント」を専用テーブル(Outbox)へ記録する。別プロセスがそのテーブルを監視して非同期にメッセージを送信することで、「DBへの保存が成功したなら、いつかかならずイベントが発火する」という保証を作る。
難易度は高いが、本書はこのパターンをステップを追って実装例として示している。
5. 「概念は分かる、でも書けない」に効く
「値オブジェクトや集約という言葉は説明できるのに、自分のプロジェクトのどのクラスに何を持たせるかとなると手が止まる」——DDDの入門書を読み終えた開発者がよくぶつかる壁だ。本書はオンライン書店という一つの題材を最後までコードで作り切ることで、この概念と実装の距離を詰める。集約の境界で迷ったときに、Order が Product を丸ごと抱えず ProductId だけを持つという判断を理由つきで見せるように、抽象論ではなく目の前のコードへそのまま写せる一例が示される。「正解の一例」を一度自分の目で確かめたい段階に効く。
6. こんな人に向いている、そうでない人
DDDの入門書(エヴァンスのDDD本や『ドメイン駆動設計をはじめよう』など)をひと通り読み終え、「実際のプロジェクトでどう適用するか」で詰まっている中〜上級者のバックエンドエンジニアに最適だ。設計パターンを概念として知っているが、実装に落とし込む際の判断基準や具体例が欲しいという段階に、本書はちょうど合う。
逆に、DDDの用語自体がまだ初見で、最初の一冊を選ぶ段階の人には早い。本書は入門書を読み終えた層を前提に実装へ踏み込むため、まず『ドメイン駆動設計をはじめよう』などで概念の地図を持ってから戻るほうが得るものが大きい。また本書は難解さを避けるため、業界で「ユビキタス言語」「境界づけられたコンテキスト」と呼ばれる用語を、それぞれ「同じ言葉」「区切られた文脈」と平易に訳し替えている。エヴァンス本などで標準用語に慣れ、他文献との接続を重視する人は、この訳語のズレに引っかかりやすいという指摘がある。性能指標や災害復旧といった非機能要件に基づく設計判断の具体例も主眼から外れており、そこまで求める読者には手薄に映る。
7. 読んだ後、最初にやる一手
本書のパターンを一度に全部持ち込もうとすると挫折しやすい。最初の一歩は、手元のコードから「ゲッターとセッターしか持たないクラス」を一つ選び、そこに散らばったバリデーションや計算を値オブジェクトへ引き取ってみることだ。商品名の文字数チェックのような小さなルールで構わない。手応えが出たら次に、更新のたびに無関係なデータまで読み込む巨大な集約を一つ探し、他の集約への参照を ID に置き換えてトランザクション境界を縮められないか検討する。Outbox パターンやイベントソーシングのような重い仕組みは、この二つで感触を掴んでからで遅くない。題材の GitHub コードを実際に動かし、詰まった箇所だけ本文へ戻る——写経で終わらせないコツはそこにある。
筆者の体験から
エヴァンスの原典から入ろうとして、値オブジェクトや集約の定義は追えても自分のコードのどこに何を持たせるべきか像が結べず、一度離れた。テックリードとして設計レビューに立つようになってからは、「なぜこの集約分割が良いのか」を経験則以上の言葉で説明できないことがもどかしかった。
読んだ直後、管理画面のコードレビューで、本書が説明していた状況をそのまま再現したような実装に行き当たった。ある商品エンティティが、関連する注文履歴を配列で丸ごと抱えていたのだ。書いた本人は「一覧表示が楽だから」と言ったが、1件更新するたびに数百件の注文データが道連れでロードされていた。本書のID参照の話がちょうど頭に入っていたので、注文オブジェクトの代わりにOrderIdの配列だけを持たせる改修を提案した。本人は最初「回りくどくなる」と渋っていたが、トランザクション競合が減った結果を見て、次の機能では自分から同じ判断をするようになった。
Outboxパターンは手を動かして理屈は理解したが、自チームの規模で非同期基盤を新設するほどの切実さはまだなく、導入は保留にしている。
DevBookPath のマップで確認する
この本の前後の読書順は、DevBookPath のグラフで確認できます。
本記事のリンクには Amazon アソシエイト等の広告が含まれる場合があります。リンク経由の購入で運営者に紹介料が支払われることがあります。
この記事を共有
この地図を共有