DDDとWebバックエンドの相性
私はドメイン駆動設計(DDD)の「戦術的テクニック」と「RDBを含むWebバックエンド」の組み合わせに違和感を感じています。
DDDには「戦略的設計」と「戦術的テクニック」があります。
戦略的設計はドメイン知識をコードに落とし込むという大きな方向性で、戦術的テクニックはそれを実現するための具体的な実装手法です。
本記事では、なぜ戦術的テクニック(エンティティ、集約、リポジトリなど)とWebバックエンドの相性が悪いのか、自身の経験を踏まえて説明します。
なお、ドメイン知識をコードに落とし込むという戦略的設計の思想自体は素晴らしいものです。
ここで問題にしているのは、その実装方法についてです。
🟦 ソフトウェアの形態とDDDの相性
DDDが提唱された2003年(Eric Evansの著書発行年)当時、エンティティパターンが最も自然に機能する環境の典型例として、Microsoft OfficeやPhotoshopのようなスタンドアロンなデスクトップアプリケーションが挙げられます。
🟠 デスクトップアプリケーションの特徴
デスクトップアプリケーションでは
- アプリケーション起動時にファイルからメモリに状態を読み込む
- メモリ上で状態を保持し続ける
- ユーザー操作のたびにメモリ上のデータを更新
- 保存時にメモリからファイルに書き出す
この環境では、DDDのエンティティパターンが自然に機能します。
メモリ上に常駐するオブジェクトがビジネスルールを守りながら状態を管理できるからです。
図: デスクトップアプリでエンティティを扱う場合の処理の流れ
🟠 Webバックエンドの本質的な違い
一方、Webバックエンドには以下の特徴があります
-
ステートレス性
- HTTPはリクエスト間で状態を保持しない(リクエストごとにメモリが揮発する)
- 状態はRDBが保持している
-
データ量の制約
- 全データをメモリに載せることができない
- 必要な部分だけを都度読み込む必要がある
🟦 エンティティパターンの非効率性
Webバックエンドでエンティティパターンを適用すると、以下の3ステップが必要になります
- DBから集約・エンティティを復元
- エンティティのメソッドを呼び出して状態を更新
- 変更されたエンティティをDBに保存
この3ステップをリクエストのたびに実行することは、単純な更新処理には過剰です。
例えば、ユーザーのメールアドレスを更新するだけなのに、ユーザーエンティティ全体を復元する必要があるでしょうか?
図: Webバックエンドでエンティティを扱う場合の処理の流れ
🟦 他のデータとの整合性チェックの問題
🟠 メールアドレスの重複チェック
よくある例として、メールアドレスの重複チェック(他のユーザーとの整合性チェック)を考えてみます。
エンティティ内で完結させようとすると
class User {
changeEmail(newEmail: string) {
// ここで他のユーザーのメールアドレスと比較したいが...
// エンティティ内では他のエンティティを参照できない
}
}
結局、リポジトリやドメインサービスに処理を委譲することになり、エンティティの責務が曖昧になります。
🟠 集約の肥大化
他のデータとの整合性チェックを集約内で完結させようとすると、集約が不必要に大きくなる「集約の肥大化」という問題が発生します。
例えば、ECサイトの注文処理を考えてみましょう。
純粋なDDDの考え方では、注文時の在庫チェックは注文集約内で行うべきです。そのためには、注文集約に全商品の在庫情報を含める必要があります。これにより、整合性を保ちながら集約内で在庫チェックを完結させることができます。
しかし、このアプローチは現実的ではありません。次のような深刻な問題が発生するためです。
- メモリに乗り切らない: 数万点の商品情報を1つの集約に含めると、メモリ使用量が爆発的に増加します(例:10万商品 × 在庫情報 = 数GB規模)。
- ロックが大きくなる: 在庫確認のたびに巨大な集約全体がロックされるため、並行性が著しく低下し、多くの注文を同時に処理できなくなります。
このため、現実的にはドメインサービスやリポジトリを使って在庫チェックを行うことになります。これは、エンティティや集約だけでビジネスルールを完結させるという理想から外れることを意味します。
🟦 クエリにもドメイン知識が含まれる
「リポジトリにはドメイン知識が含まれない」という理想論がありますが、現実は異なります。
例えば、「アクティブなユーザーを取得する」という要件があったとき
SELECT * FROM users
WHERE deleted_at IS NULL
AND last_login_at > NOW() - INTERVAL '30 days'
AND email_verified = true
このWHERE句の条件こそがドメイン知識です。
これをエンティティ側で表現しようとすると、全ユーザーを取得してからフィルタリングすることになり、非現実的です。
🟦 近年のWeb開発環境の変化
🟠 APIの粒度の細分化
かつてのMVCアーキテクチャでは、1つのエンドポイントで画面全体のデータを扱っていました。
近年のフロント + バックエンドの構成では
- APIの粒度が小さくなった(1つのAPIは単純な処理のみを担当)
- 複数のAPIを組み合わせて1つの業務を実現
この変化により、個々のAPIでDDDの複雑な仕組みを使う必要性は低下しています。
🟠 ドメインエキスパートの不在
DDDは「ドメインエキスパートとの対話」を重視しますが、近年では
- システムから新しい業務が生まれることが多い
- 開発者自身がドメインエキスパートになる必要がある
- 業務とシステムが相互に影響し合う
具体的な例として、UberやAirbnbのようなプラットフォームビジネスを考えてみましょう。
これらのサービスでは
従来の業務との違い
- 「ライドシェア」や「民泊」という業務自体がシステムによって生み出された
- 既存のタクシー業界や宿泊業界の知識だけでは不十分
- アルゴリズムによる価格設定やマッチングが業務の中核
システムが生み出した新しい概念
- サージプライシング(需給に応じた動的価格設定)
- レーティングシステムによる信頼性の担保
- リアルタイムマッチングアルゴリズム
このような場合、従来の「業務エキスパートから知識を抽出してモデリング」というアプローチは機能しません。
システムが新しいビジネスモデルを可能にします。
そのモデルが市場に受け入れられることで、開発者自身がビジネスルールを定義する立場になります。
それが成功すれば、新たな業務として定着していくのです。
🟦 インフラストラクチャ層の制約による複雑性
Webバックエンドのプログラムが複雑になる原因の多くは、インフラストラクチャ層の制約に起因します。
この複雑さは、純粋なドメインモデリングだけでは導き出せないものです。
🟠 インフラ都合による処理の複雑化
よくある例として、N+1問題を考えてみましょう。
「ユーザーとその投稿を表示する」という単純な要件でも、素朴に実装するとユーザー数分のクエリが発生します
// N+1問題が発生する例
const users = await getUserList();
for (const user of users) {
// ユーザー数分のクエリが発生
user.posts = await getPostsByUserId(user.id);
}
これを回避するために、以下のような対策が必要になります
// N+1問題を回避する例
const users = await getUserList();
const userIds = users.map(u => u.id);
// 先に全ユーザーの投稿を一括取得
const allPosts = await getPostsByUserIds(userIds);
// メモリ上で結合
users.forEach(user => {
user.posts = allPosts.filter(p => p.userId === user.id);
});
このような「先に一括で取得してメモリ上で結合」という処理順は、ビジネス要件から直感的に導かれるものではありません。
しかし、これはシステムを効率的に動作させるために必要な知識です。
🟠 この複雑さもドメイン知識である
インフラストラクチャ層の制約から生まれる複雑さは、純粋なビジネスロジックではないかもしれません。
しかし、システムの現実的な運用には不可欠な知識です。
他にも
- AWS S3へのファイルアップロード時、Presigned URLの利用による通信回数の増加
- 外部APIのクォータ上限を考慮した処理タイミングの調整
- バッチ処理においてメモリ上限を考慮したページング処理
これらは全て、システムを正しく動作させるために必要な「ドメイン知識」として、チーム内で共有されるべきものです。
DDDの理想では、これらの技術的制約をインフラストラクチャ層に隠蔽することを目指しますが、
現実にはドメイン層の設計自体がこれらの制約に大きく影響を受けます。
この現実を受け入れ、技術的制約も含めた統合的なモデリングが必要となるのです。
🟦 レイヤー間の冗長性
DDDでは各レイヤーで独自の型を定義することが推奨されますが、これにより
- 似たような構造体を複数定義する必要がある(例:UserDTO、UserEntity、UserResponse)
- レイヤー間でのデータの詰め替えが発生
- 詰め替えミスによるバグのリスク
- 保守性の低下
実際の開発では、この冗長性に見合うメリットを感じることは稀です。
🟦 学習コストと実装コストの問題
DDDを適切に実装するには
- チーム全員がDDDの概念を理解する必要がある
- 設計の議論に多くの時間を費やす
- 「ある処理をどのエンティティに実装するか」で意見が分かれる
- ルールの遵守状況をレビューで確認する必要がある
これらのコストに見合うメリットを感じることは稀です。
🟦 まとめ
🟠 エンティティのメッセージパッシングは現実的でない
本記事の核心的なメッセージは、「エンティティのメッセージパッシングで業務を表現する」というアプローチがWebバックエンドでは現実的でないということです。
その理由はこれまでに述べた通りです。
DDDの戦術的なテクニックは、メモリ上で状態を保持し続けるアプリケーションには適していますが、ステートレスなWebバックエンドとは相性が悪いのです。
🟠 ドメイン知識を表現する代替アプローチ
コードだけで表現しようとするのではなく、以下のようなアプローチも検討できます
-
テストコードの作成・維持に力を注ぐ
-
データモデリングに魂を込める
- テーブル設計、制約でドメイン知識を表現する
- テーブルのドキュメントを作成・維持する
- レコードのライフサイクルを明記する(いつ作成され、いつ削除されるのかを明確にする)
- 複雑な仕様や紛らわしい仕様があるのであればそれらを明記しておく
- カラムがどのような値をとり得るのか明記する
-
情報を整理・可視化し、チームの形式知として昇華させるスキルを磨く
- ドメイン知識を構造化して伝える技術を習得する
- 図表やドキュメントによる効果的な知識共有を目指す
- 参考書籍
🟠 DDDは手段であって目的ではない
重要なのは、「バグを減らす」「開発効率を上げる」という本来の目的を見失わないことです。
ドメイン知識をどこかに表現することは重要ですが、それが必ずしもエンティティのメソッドである必要はありません。
プロジェクトの特性に応じて、最も適切な実装方法を選択することが、真の意味でのドメイン駆動と言えるでしょう。
(余談)最後まで読んで頂きありがとうございます!
内容に共感頂けたら いいね or リツイート 頂けると励みになります 🙏
Discussion