mybook

ドメイン駆動設計とは — DDDの全体像

リナはエリック・エヴァンスの "Domain-Driven Design" を読み始めた。2003年に出版されたこの本は、「ブルーブック」と呼ばれ、ソフトウェア設計の古典として今なお参照され続けている。

最初の数ページで「ドメインとは何か」が明確に定義されていた。

ドメインとは何か

ドメインとは、ソフトウェアが解決しようとしている問題領域のことだ。FreshCartであれば「食料品のオンライン販売」がドメインになる。

「ドメイン」という言葉は日常語でも「専門分野」「領域」という意味を持つ。弁護士のドメインは「法律」、医師のドメインは「医学」だ。ソフトウェアのドメインは「そのシステムが解決しようとしているビジネスの問題領域」だ。

より具体的には:

  • 注文管理 — 注文の受付、確認、変更、キャンセルのルール
  • 在庫管理 — 在庫の追跡、引き当て、補充、廃棄のルール
  • 配送管理 — 配送先、配送業者の選択、追跡、不在時の処理
  • 決済処理 — クレジットカード、代引き、ポイント払い、返金のルール
  • 顧客管理 — 顧客情報の管理、会員ランク、住所帳

これらは全て「食料品販売」という大きなドメインの中のサブドメインだ。

INFO

DDDの根本的な目標は「ソフトウェアの構造がビジネスの構造を反映すること」だ。コードを読めばビジネスルールが理解でき、ビジネスを理解すればコードの構造が予測できる状態を目指す。

なぜDDDが生まれたか

1990年代から2000年代初頭、オブジェクト指向プログラミングが普及するにつれ、ソフトウェアはより複雑になっていった。技術的な複雑さは設計パターン(GoF)で対処できたが、ビジネスの複雑さへの対処法は明確でなかった。

エリック・エヴァンスは様々なプロジェクトを経験する中で気づいた。失敗したプロジェクトの多くは、技術的な問題ではなく「ビジネスとコードの乖離」が原因だった。エンジニアとビジネス側が異なる言語で話し、コードがビジネスの複雑さを表現できていなかった。

DDDはこの問題への回答として生まれた。

DDDの2つの柱

DDDには大きく2つのカテゴリがある。

Loading diagram...

戦略的パターン — 「どこに何を置くか」

戦略的パターンは、システム全体の設計に関わる。「このビジネス機能はどのシステムに属するか」「チーム間でどう役割分担するか」といった大局的な問いに答える。

パターン目的
ユビキタス言語チームとコードで同じ言語を使う
境界づけられたコンテキストドメインを明示的な境界で分割する
コンテキストマップコンテキスト間の関係を可視化する

戦略的パターンは「コードを書く前」に考える。FreshCartで言えば「注文処理と在庫管理は別のコンテキストにすべきか」「外部の決済APIとの境界をどこに引くか」という設計判断だ。

戦術的パターン — 「どのように実装するか」

戦術的パターンは、個々のコンポーネントの実装に関わる。コードレベルでドメインモデルをどう表現するかを定義する。

パターン目的
エンティティIDで識別されるオブジェクト
値オブジェクト属性で定義されるイミュータブルなオブジェクト
集約整合性の境界をもつオブジェクトのクラスタ
リポジトリ集約の永続化を抽象化する
ドメインサービスエンティティに属さないドメインロジック
ドメインイベントドメイン内で起きた出来事を表現する

戦術的パターンは「コードを書く時」に考える。FreshCartで言えば「Money を値オブジェクトとして実装する」「OrderOrderItem を集約として設計する」という実装判断だ。

ドメインモデルとは

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はすべてのプロジェクトに適しているわけではない。

Loading diagram...

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の適用計画を立てた。一度にすべてを変えるのではなく、段階的に進める。

Loading diagram...

各ステップは独立しており、段階的に適用できる。

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
end

ActiveRecordを「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のドメインモデル概要を示す。詳細は各章で学ぶ。

Loading diagram...

各コンテキストは独立したモジュールとして実装され、明確な境界を持つ。コンテキスト間の通信はインターフェース(ACL)を通じて行われる。

INFO

この図は13章を通じて徐々に肉付けされる。最初は概念図だが、各章で具体的なコードとして実装される。この全体像を頭に入れながら学ぶと、個々のパターンの意味が見えやすくなる。

まとめ

  • DDD = ソフトウェアの構造をビジネスの構造に合わせるアプローチ
  • 戦略的パターン = システム全体の設計(ユビキタス言語、境界づけられたコンテキスト)
  • 戦術的パターン = コンポーネントの実装(エンティティ、値オブジェクト、集約など)
  • 適用判断 = 複雑なドメイン・大きなチーム・長期メンテで有効
  • Railsとの共存 = ActiveRecordはORM、ドメインオブジェクトはビジネスロジック、リポジトリが橋渡し
  • コスト = 学習コスト・初期開発速度低下・コード量増加。ROIで判断する

次の章では、DDDの最も基本的かつ重要な概念「ユビキタス言語」を深く掘り下げる。「在庫引き当て」という言葉がコードに現れる瞬間を、リナと一緒に体験しよう。