ドメイン駆動設計とは — DDDの全体像
リナはエリック・エヴァンスの "Domain-Driven Design" を読み始めた。2003年に出版されたこの本は、「ブルーブック」と呼ばれ、ソフトウェア設計の古典として今なお参照され続けている。
最初の数ページで「ドメインとは何か」が明確に定義されていた。
ドメインとは何か
ドメインとは、ソフトウェアが解決しようとしている問題領域のことだ。FreshCartであれば「食料品のオンライン販売」がドメインになる。
「ドメイン」という言葉は日常語でも「専門分野」「領域」という意味を持つ。弁護士のドメインは「法律」、医師のドメインは「医学」だ。ソフトウェアのドメインは「そのシステムが解決しようとしているビジネスの問題領域」だ。
より具体的には:
- 注文管理 — 注文の受付、確認、変更、キャンセルのルール
- 在庫管理 — 在庫の追跡、引き当て、補充、廃棄のルール
- 配送管理 — 配送先、配送業者の選択、追跡、不在時の処理
- 決済処理 — クレジットカード、代引き、ポイント払い、返金のルール
- 顧客管理 — 顧客情報の管理、会員ランク、住所帳
これらは全て「食料品販売」という大きなドメインの中のサブドメインだ。
INFO
DDDの根本的な目標は「ソフトウェアの構造がビジネスの構造を反映すること」だ。コードを読めばビジネスルールが理解でき、ビジネスを理解すればコードの構造が予測できる状態を目指す。
なぜDDDが生まれたか
1990年代から2000年代初頭、オブジェクト指向プログラミングが普及するにつれ、ソフトウェアはより複雑になっていった。技術的な複雑さは設計パターン(GoF)で対処できたが、ビジネスの複雑さへの対処法は明確でなかった。
エリック・エヴァンスは様々なプロジェクトを経験する中で気づいた。失敗したプロジェクトの多くは、技術的な問題ではなく「ビジネスとコードの乖離」が原因だった。エンジニアとビジネス側が異なる言語で話し、コードがビジネスの複雑さを表現できていなかった。
DDDはこの問題への回答として生まれた。
DDDの2つの柱
DDDには大きく2つのカテゴリがある。
戦略的パターン — 「どこに何を置くか」
戦略的パターンは、システム全体の設計に関わる。「このビジネス機能はどのシステムに属するか」「チーム間でどう役割分担するか」といった大局的な問いに答える。
| パターン | 目的 |
|---|---|
| ユビキタス言語 | チームとコードで同じ言語を使う |
| 境界づけられたコンテキスト | ドメインを明示的な境界で分割する |
| コンテキストマップ | コンテキスト間の関係を可視化する |
戦略的パターンは「コードを書く前」に考える。FreshCartで言えば「注文処理と在庫管理は別のコンテキストにすべきか」「外部の決済APIとの境界をどこに引くか」という設計判断だ。
戦術的パターン — 「どのように実装するか」
戦術的パターンは、個々のコンポーネントの実装に関わる。コードレベルでドメインモデルをどう表現するかを定義する。
| パターン | 目的 |
|---|---|
| エンティティ | IDで識別されるオブジェクト |
| 値オブジェクト | 属性で定義されるイミュータブルなオブジェクト |
| 集約 | 整合性の境界をもつオブジェクトのクラスタ |
| リポジトリ | 集約の永続化を抽象化する |
| ドメインサービス | エンティティに属さないドメインロジック |
| ドメインイベント | ドメイン内で起きた出来事を表現する |
戦術的パターンは「コードを書く時」に考える。FreshCartで言えば「Money を値オブジェクトとして実装する」「Order と OrderItem を集約として設計する」という実装判断だ。
ドメインモデルとは
DDDの核心にある概念が「ドメインモデル」だ。これは特定のドメインに対する抽象化された表現であり、図であったりコードであったり、チームの共有知識であったりする。
重要なのは、ドメインモデルはデータベースのスキーマではないということだ。ドメインモデルはビジネスの「振る舞い」と「ルール」を表現する。
# ドメインモデルの例 — OrderStatus(注文状態)
# これはデータベースの文字列カラムではなく、ビジネスルールを表現する
module OrderContext
class OrderStatus
PENDING = new('pending')
CONFIRMED = new('confirmed')
SHIPPED = new('shipped')
DELIVERED = new('delivered')
CANCELLED = new('cancelled')
# ビジネスルール: どの状態からどの状態に遷移できるか
VALID_TRANSITIONS = {
PENDING => [CONFIRMED, CANCELLED],
CONFIRMED => [SHIPPED, CANCELLED],
SHIPPED => [DELIVERED],
DELIVERED => [],
CANCELLED => []
}.freeze
attr_reader :value
def initialize(value)
@value = value
freeze
end
def can_transition_to?(next_status)
VALID_TRANSITIONS[self].include?(next_status)
end
def to_s
value
end
end
endこのコードは単純な文字列よりも多くを語っている。注文のライフサイクル——どの状態からどこへ移れるか——がコードに明示されている。
DDDが適切な場面
DDDはすべてのプロジェクトに適しているわけではない。
DDDが特に有効な状況:
- ドメインが複雑: ビジネスルールが多く、例外処理が頻繁に発生する
- チームが大きい: 複数チームが同じシステムを扱い、コミュニケーションコストが高い
- 長期メンテナンス: 要件が頻繁に変化し、変更容易性が重要
- ビジネス価値が高い: コアビジネスロジックを正確に表現する必要がある
WARNING
DDDはオーバーエンジニアリングになりがちだ。シンプルなCRUDアプリや短期プロジェクトにDDDを適用すると、むしろ開発コストが増大する。「お知らせ管理」「FAQページ」のような単純な機能にDDDは不要だ。適用判断は慎重に。
FreshCartへの適用判断
リナはFreshCartを3つの観点で分析した。
ドメインの複雑さ
FreshCartのビジネスルールを洗い出すと、想定以上に複雑だった。
注文管理:
- カート追加 → 在庫仮押さえ
- 注文確定 → 在庫確定引き当て → 決済処理 → 配送準備
- 注文キャンセル(確定前)→ 在庫解放
- 注文キャンセル(確定後、配送前)→ 在庫解放 → 返金処理
- 配送開始後のキャンセル → 返金は可能だが在庫は戻らない
割引・プロモーション:
- 会員ランク(ゴールド/シルバー/ブロンズ)による基本割引
- 注文金額による送料無料しきい値(3,000円以上)
- 離島・沖縄への追加送料
- キャンペーンコード(期限付き、使用回数制限あり)
- 初回注文特典(10%割引)
- 複数プロモーションの重複適用ルール
在庫管理:
- 通常在庫 vs 予約在庫 vs 引き当て済み在庫の区別
- 賞味期限による自動廃棄処理
- 低在庫アラートしきい値(商品カテゴリごとに異なる)
- セット商品(複数商品を1パッケージとして販売)
これは複雑だ。単純なCRUDでは表現しきれない。
チームの状況
8名のエンジニアが同じRailsモノリスを触っている。コードのオーナーシップが曖昧で、変更の影響範囲が把握できない。これはDDDの明示的な境界設計が効く状況だ。
将来の拡張性
FreshCartは5年後に20万SKUを扱う計画がある。マイクロサービス化の可能性も検討中だ。今から境界を明確にしておくことが、将来の技術的選択肢を広げる。
結論: FreshCartはDDDの適用対象として適切だ。
DDDの旅のロードマップ
リナはDDDの適用計画を立てた。一度にすべてを変えるのではなく、段階的に進める。
各ステップは独立しており、段階的に適用できる。
Railsとの関係
多くのRails開発者が心配するのは「ActiveRecordとDDDは相性が悪いのでは」という点だ。
ActiveRecordはデータとビジネスロジックを同じクラスに置く「Active Recordパターン」を採用している。DDDの「ドメインオブジェクトはインフラを知らない」という原則と衝突するように見える。
実際には、ActiveRecordとDDDは共存できる。重要なのは役割分担だ。
# Before: ActiveRecordにすべてが混在
class Order < ApplicationRecord
def confirm
# 在庫、決済、配送、メール、ポイント...すべてここに
# 500行の地獄
end
end
# After: 役割を分担する(概念図)
module OrderContext
class Order # ドメインオブジェクト: ビジネスロジックのみ
def confirm!
raise InvalidStateTransition unless can_confirm?
@status = OrderStatus::CONFIRMED
add_event(OrderConfirmed.new(order_id: id))
self
end
end
end
class OrderRecord < ApplicationRecord # ActiveRecord: データアクセスのみ
belongs_to :customer_record
has_many :order_item_records
validates :status, presence: true
scope :pending, -> { where(status: 'pending') }
end
class OrderRepository # リポジトリ: 橋渡し役
def find(id)
record = OrderRecord.includes(:order_item_records).find!(id)
reconstruct_domain_object(record)
end
def save(domain_order)
record = OrderRecord.find_or_initialize_by(id: domain_order.id)
record.update!(status: domain_order.status.to_s)
end
endActiveRecordを「ORMとして」使いながら、ビジネスロジックをドメインオブジェクト側に配置する。リポジトリがその橋渡しをする。この分離が、DDDとRailsを共存させる鍵だ。
INFO
この本を通じて、FreshCartのコードを段階的にDDDの考え方に合わせて変えていく。完璧なDDDを目指すのではなく、「コードがビジネスを語れる」状態を目指すことが目標だ。段階的に、チームが理解できるペースで進める。
DDDの核心思想
最後に、DDDの核心思想を3つにまとめる。
1. ドメインの複雑さに向き合う
ビジネスの複雑さを「技術で隠す」のではなく、「コードで表現する」。複雑なビジネスルールは複雑なコードになって当然だが、その複雑さが「理解できる形」で表現されているべきだ。
2. 言語の共有
エンジニアとビジネス側が同じ言語を使う。コードの中にビジネスの用語が現れる。会議での言葉とコードが対応している。
3. モデルの継続的改善
ドメインモデルは完成形ではない。ビジネスの理解が深まるにつれ、モデルも進化する。DDDは「設計してから実装する」ではなく「実装しながら設計を改善する」ための継続的なプロセスだ。
# ドメインモデルの進化の例
# 最初のバージョン
class Order
def confirm!
@status = :confirmed
end
end
# 理解が深まった後のバージョン
# 「注文はカートアイテムがないと確定できない」というビジネスルールが明確になった
class Order
def confirm!
raise EmptyOrderError, "注文商品が1つもありません" if order_items.empty?
raise InvalidStateError, "#{@status}状態の注文は確定できません" unless pending?
@status = OrderStatus::CONFIRMED
@confirmed_at = Time.current
add_event(OrderConfirmed.new(
order_id: id,
confirmed_at: @confirmed_at
))
self
end
endビジネスを深く理解するほど、モデルは豊かになる。
DDDを導入するコスト
DDDはメリットが大きい一方、コストも明確に存在する。リナは導入前にコストを正直に評価した。
学習コスト
DDDの概念は独自の用語が多い。「集約ルート」「ユビキタス言語」「境界づけられたコンテキスト」といった言葉は直感的でない。チーム全員がある程度理解するまでに、1〜2ヶ月かかる可能性がある。
開発速度の一時的な低下
最初の1〜2ヶ月は、新しいパターンを適用しながら開発するため、一時的に開発速度が下がる。「以前は Order.confirm! を呼ぶだけで良かったのに」という声が出るかもしれない。
コードの量が増える
ドメインオブジェクト、リポジトリ、ユースケースと、役割ごとにクラスが分かれる。コードの総行数は増える。「なんでこんなに複雑にするの?」と疑問を持つ人も出る。
# Before: シンプルに見える(でも問題が隠れている)
def create
order = Order.new(order_params)
order.save!
order.confirm! # ← 内部で100行の副作用
render json: order
end
# After: 少し多く見える(でも責務が明確)
def create
result = PlaceOrderUseCase.new(
order_repository: Container[:order_repository],
catalog_adapter: Container[:catalog_adapter]
).call(place_order_command)
if result.success?
render json: OrderPresenter.new(result.value).to_json, status: :created
else
render json: { error: result.error_message }, status: :unprocessable_entity
end
end見た目上の複雑さは増えているが、各クラスの責務が明確になり、テストが書きやすくなっている。
ROIの計算
リナはチームに対してROI(投資対効果)を数字で示した。
初期コスト(3ヶ月):
- 学習: 8人 × 4時間/週 × 12週 = 384時間
- リファクタリング: 160時間(週20時間 × 8週)
合計: 約544時間
期待されるリターン(12ヶ月):
- バグ減少: 月8件 → 2件 = 6件/月削減
バグ対応コスト: 4時間/件 → 節約: 24時間/月 × 12 = 288時間
- 機能実装の高速化: 3日 → 1.5日 = 1.5日/機能 × 40機能/年 = 60日 = 480時間
- オンボーディング短縮: 2週間 → 1週間 × 年2名採用 = 80時間
合計: 約848時間
ROI: 848 / 544 = 1.56 (12ヶ月で156%のリターン)
もちろんこれは見積もりだ。しかし数字にすることで、ビジネスサイドの承認を得やすくなる。
INFO
DDDの導入コストを正直に見積もり、それをチームとビジネスサイドに提示することが大切だ。「技術的には良いから」という理由だけでは承認が得にくい。ROIを示すことで、組織として投資の判断ができる。
タカシとの議論 — 本当にDDDが必要か
「DDDって、銀の弾丸じゃないよね」
タカシが言った。DDDを提案したのはタカシ自身だが、かといって盲目的に賛成するわけではない。
「前の会社でDDDをやって、失敗したプロジェクトも見た。ドメインオブジェクトを作りすぎて、ActiveRecordのシンプルさを失ったんだ。Railsの規約を捨てて、Javaみたいなアーキテクチャにしてしまった」
リナはうなずいた。「それは確かに罠だと思う。だから私たちは、Railsの良さを活かしながら、足りない部分だけDDDを補完する形で使いたい」
2人は白板に書き出した。
Railsが得意なこと(そのまま使う):
✅ ActiveRecordによるDBアクセス
✅ コントローラーのルーティング
✅ バリデーション(DBレベル)
✅ マイグレーション
✅ テストフレームワーク(RSpec)
✅ メーラー
DDDで補完すること:
🔧 ビジネスロジックの置き場所(ドメインオブジェクト)
🔧 状態遷移ルール(値オブジェクト)
🔧 集約の境界(どのデータを1トランザクションで扱うか)
🔧 外部APIとの境界(ACL)
🔧 チームの共通言語(ユビキタス言語)
「この分担なら、RailsのスピードとDDDの設計の良さを両立できると思う」
タカシは首を縦に振った。「段階的に始めるなら試す価値はある。最初からコントローラーをUseCaseに変える必要はない。まず値オブジェクトを1つ作って、チームが慣れてから次に進もう」
この会話がFreshCartのDDD導入の出発点になった。
DDDを支えるプログラミング原則
DDDは「設計哲学」だが、その背後にはいくつかのプログラミング原則がある。これらを理解しておくと、なぜDDDがそのようなパターンを採用しているのかが明確になる。
単一責務原則(SRP)
「クラスを変更する理由は1つであるべき」
FreshCartの Order#confirm! には変更理由が8つある(在庫更新、メール送信、ポイント付与、配送準備、分析、キャッシュ削除、決済処理、アラート)。これは SRP 違反だ。
DDDでは:
OrderContext::Order— 注文状態の変化のみ(変更理由: ビジネスルールの変更)StockReservationService— 在庫引き当てのみ(変更理由: 在庫ビジネスルールの変更)ConfirmOrderUseCase— 処理の調整のみ(変更理由: ユースケースの手順変更)
依存性逆転の原則(DIP)
「上位モジュールは下位モジュールに依存すべきでない。両方が抽象に依存すべき」
# 依存性逆転の原則を適用しない場合
class ConfirmOrderUseCase
def call(order_id:)
order = ActiveRecordOrderRepository.new.find(order_id) # 具体実装に依存
order.confirm!
ActiveRecordOrderRepository.new.save(order) # 具体実装に依存
end
end
# 依存性逆転の原則を適用した場合
class ConfirmOrderUseCase
def initialize(order_repository:) # インターフェースに依存
@order_repository = order_repository
end
def call(order_id:)
order = @order_repository.find(order_id) # 何のリポジトリかは知らない
order.confirm!
@order_repository.save(order)
end
end
# テスト時はInMemoryを注入
use_case = ConfirmOrderUseCase.new(order_repository: InMemoryOrderRepository.new)
# 本番時はActiveRecordを注入
use_case = ConfirmOrderUseCase.new(order_repository: ActiveRecordOrderRepository.new)テルデメテルの法則(LoD)
「オブジェクトは直接の友人(依存関係)とのみ会話すべき」
# 違反: "列車の衝突(Train Wreck)"
order.customer.address.prefecture.tax_rate
# 準拠: 必要な情報を返すメソッドを定義する
order.delivery_tax_rate # ← Order が内部で計算して返すこれらの原則はDDDのパターン設計の根拠になっている。「なぜリポジトリが必要なのか」「なぜ集約ルートを経由するのか」という疑問も、これらの原則で説明できる。
FreshCartのドメインモデル概要図
リナが最終的に設計したFreshCartのドメインモデル概要を示す。詳細は各章で学ぶ。
各コンテキストは独立したモジュールとして実装され、明確な境界を持つ。コンテキスト間の通信はインターフェース(ACL)を通じて行われる。
INFO
この図は13章を通じて徐々に肉付けされる。最初は概念図だが、各章で具体的なコードとして実装される。この全体像を頭に入れながら学ぶと、個々のパターンの意味が見えやすくなる。
まとめ
- DDD = ソフトウェアの構造をビジネスの構造に合わせるアプローチ
- 戦略的パターン = システム全体の設計(ユビキタス言語、境界づけられたコンテキスト)
- 戦術的パターン = コンポーネントの実装(エンティティ、値オブジェクト、集約など)
- 適用判断 = 複雑なドメイン・大きなチーム・長期メンテで有効
- Railsとの共存 = ActiveRecordはORM、ドメインオブジェクトはビジネスロジック、リポジトリが橋渡し
- コスト = 学習コスト・初期開発速度低下・コード量増加。ROIで判断する
次の章では、DDDの最も基本的かつ重要な概念「ユビキタス言語」を深く掘り下げる。「在庫引き当て」という言葉がコードに現れる瞬間を、リナと一緒に体験しよう。