【DDD】ドメイン駆動設計 完全ガイド - OOPから関数型まで

PUBLISHED 2026-02-05

DDD(Domain-Driven Design、ドメイン駆動設計)は、複雑なビジネスロジックを持つシステムの設計に適した手法です。本記事では、DDDの概念から実装方法まで、OOP(オブジェクト指向)と関数型の両方のアプローチで解説します。

DDDとは

DDD(Domain-Driven Design、ドメイン駆動設計)は、Eric Evansが2003年に提唱したソフトウェア設計手法です。

核心的な考え方

ビジネスドメイン(業務領域)を中心に設計するという思想です。技術ではなく、ビジネスの問題領域を深く理解し、それをコードに反映させます。

主要な概念

戦略的設計(大きな構造)

概念説明
ユビキタス言語開発者とドメインエキスパートが共通で使う用語
境界づけられたコンテキストモデルが適用される明確な境界
コンテキストマップ複数のコンテキスト間の関係を図示

戦術的設計(実装パターン)

パターン説明
エンティティ一意のIDを持つオブジェクト(例: ユーザー、注文)
値オブジェクトIDを持たず、属性で同一性を判断(例: 住所、金額)
集約関連するエンティティ・値オブジェクトのまとまり
リポジトリ集約の永続化を担当
ドメインサービスエンティティに属さないビジネスロジック
ファクトリ複雑なオブジェクト生成を担当
📚 参考書籍

詳しく知りたい場合は、Eric Evansの「Domain-Driven Design」(通称: 青本)や、Vaughn Vernonの「実践ドメイン駆動設計」(通称: 赤本)が参考になります。

DDDに適した言語

DDDは言語に依存しない設計手法です。どの言語でも適用できます。

相性が良い言語

言語理由
Java / KotlinクラスベースOOP、豊富なDDDライブラリ・フレームワーク
C#同上、.NETエコシステムでDDD実践例が多い
TypeScript型システムでドメインモデルを表現しやすい
Scala関数型+OOPのハイブリッドで表現力が高い

適用可能だが工夫が必要な言語

言語備考
Python動的型付けだが、dataclassやPydanticで値オブジェクトを表現
Ruby同様に可能、Rails外の設計が必要になることも
Go構造体とインターフェースで実装、継承がないので工夫が必要
Rust所有権システムと組み合わせて堅牢な設計が可能

関数型言語でのDDD

F#、Haskell、Elixirなどでも適用可能です。「関数型ドメインモデリング」というアプローチがあり、代数的データ型で不正な状態を型レベルで防ぐ設計ができます。

OOPスタイルでのDDD実装

シンプルな「ECサイトの注文」を例に解説します。

フォルダ構造

src/
├── domain/                 # ドメイン層(ビジネスロジック)
│   ├── entities/           # エンティティ
│   │   ├── order.py
│   │   └── user.py
│   ├── value_objects/      # 値オブジェクト
│   │   ├── money.py
│   │   └── email.py
│   ├── repositories/       # リポジトリ(インターフェース)
│   │   └── order_repository.py
│   └── services/           # ドメインサービス
│       └── pricing_service.py

├── application/            # アプリケーション層(ユースケース)
│   └── use_cases/
│       └── create_order.py

├── infrastructure/         # インフラ層(技術的な実装)
│   └── repositories/
│       └── order_repository_impl.py

└── main.py

各層の役割

役割依存方向
domainビジネスロジック。外部に依存しないなし
applicationユースケースの実行domain
infrastructureDB、API等の技術的実装domain, application

値オブジェクト(Value Object)

IDを持たず、値で同一性を判断します。イミュータブルにするのがポイントです。

# domain/value_objects/money.py
from dataclasses import dataclass

@dataclass(frozen=True)  # frozen=True でイミュータブルに
class Money:
    amount: int
    currency: str = "JPY"

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("金額は0以上である必要があります")

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("通貨が異なります")
        return Money(self.amount + other.amount, self.currency)

使用例:

price1 = Money(1000)
price2 = Money(500)
total = price1.add(price2)  # Money(1500, "JPY")

# 同じ値なら等価
Money(100) == Money(100)  # True

エンティティ(Entity)

一意のIDを持ち、IDで同一性を判断します。

# domain/entities/order.py
from dataclasses import dataclass, field
from typing import List
from uuid import UUID, uuid4
from enum import Enum

from domain.value_objects.money import Money

class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    CANCELLED = "cancelled"

@dataclass
class OrderItem:
    product_id: str
    product_name: str
    price: Money
    quantity: int

    @property
    def subtotal(self) -> Money:
        return Money(self.price.amount * self.quantity)

@dataclass
class Order:
    id: UUID
    customer_id: UUID
    items: List[OrderItem] = field(default_factory=list)
    status: OrderStatus = OrderStatus.PENDING

    @classmethod
    def create(cls, customer_id: UUID) -> "Order":
        """ファクトリメソッド"""
        return cls(id=uuid4(), customer_id=customer_id)

    def add_item(self, item: OrderItem) -> None:
        """商品を追加"""
        if self.status != OrderStatus.PENDING:
            raise ValueError("確定済みの注文には追加できません")
        self.items.append(item)

    def confirm(self) -> None:
        """注文を確定"""
        if not self.items:
            raise ValueError("商品がありません")
        self.status = OrderStatus.CONFIRMED

    @property
    def total(self) -> Money:
        """合計金額"""
        if not self.items:
            return Money(0)
        result = Money(0)
        for item in self.items:
            result = result.add(item.subtotal)
        return result

    def __eq__(self, other):
        """IDで同一性を判断"""
        if not isinstance(other, Order):
            return False
        return self.id == other.id

リポジトリ(Repository)インターフェース

ドメイン層では「何ができるか」だけ定義します。

# domain/repositories/order_repository.py
from abc import ABC, abstractmethod
from typing import Optional
from uuid import UUID

from domain.entities.order import Order

class OrderRepository(ABC):
    """注文リポジトリのインターフェース"""

    @abstractmethod
    def save(self, order: Order) -> None:
        pass

    @abstractmethod
    def find_by_id(self, order_id: UUID) -> Optional[Order]:
        pass

リポジトリの実装(Infrastructure)

技術的な実装はインフラ層で行います。

# infrastructure/repositories/order_repository_impl.py
from typing import Optional
from uuid import UUID

from domain.entities.order import Order
from domain.repositories.order_repository import OrderRepository

class InMemoryOrderRepository(OrderRepository):
    """メモリ上で保存する実装(テスト用)"""

    def __init__(self):
        self._orders: dict[UUID, Order] = {}

    def save(self, order: Order) -> None:
        self._orders[order.id] = order

    def find_by_id(self, order_id: UUID) -> Optional[Order]:
        return self._orders.get(order_id)

ユースケース(Application)

アプリケーション層でビジネスフローを組み立てます。

# application/use_cases/create_order.py
from dataclasses import dataclass
from uuid import UUID

from domain.entities.order import Order, OrderItem
from domain.value_objects.money import Money
from domain.repositories.order_repository import OrderRepository

@dataclass
class CreateOrderInput:
    customer_id: UUID
    items: list[dict]

@dataclass
class CreateOrderOutput:
    order_id: UUID
    total: int

class CreateOrderUseCase:
    def __init__(self, order_repository: OrderRepository):
        self._order_repository = order_repository

    def execute(self, input_data: CreateOrderInput) -> CreateOrderOutput:
        # 1. 注文を作成
        order = Order.create(customer_id=input_data.customer_id)

        # 2. 商品を追加
        for item in input_data.items:
            order_item = OrderItem(
                product_id=item["product_id"],
                product_name=item["product_name"],
                price=Money(item["price"]),
                quantity=item["quantity"],
            )
            order.add_item(order_item)

        # 3. 注文を確定
        order.confirm()

        # 4. 保存
        self._order_repository.save(order)

        return CreateOrderOutput(
            order_id=order.id,
            total=order.total.amount,
        )

実行例

# main.py
from uuid import uuid4

from application.use_cases.create_order import CreateOrderUseCase, CreateOrderInput
from infrastructure.repositories.order_repository_impl import InMemoryOrderRepository

# 依存性の注入
repository = InMemoryOrderRepository()
use_case = CreateOrderUseCase(order_repository=repository)

# 注文を作成
result = use_case.execute(
    CreateOrderInput(
        customer_id=uuid4(),
        items=[
            {"product_id": "p1", "product_name": "Tシャツ", "price": 2000, "quantity": 2},
            {"product_id": "p2", "product_name": "パンツ", "price": 3000, "quantity": 1},
        ],
    )
)

print(f"注文ID: {result.order_id}")
print(f"合計: {result.total}円")
# 出力:
# 注文ID: 550e8400-e29b-41d4-a716-446655440000
# 合計: 7000円
💡 OOPスタイルのポイント

  • 値オブジェクト: @dataclass(frozen=True)
  • エンティティ: @dataclass + IDで__eq__定義
  • リポジトリ: ABCで抽象クラス定義
  • 依存性の注入: コンストラクタで渡す

関数型スタイルでのDDD実装

関数型DDDは、データと関数を分離し、イミュータブルなデータを扱うアプローチです。

OOPとの構造比較

OOP関数型
entities/types/(データ定義のみ)
services/operations/(純粋関数)
repositories/ports/ + adapters/
use_cases/workflows/(関数合成)

フォルダ構造

src/
├── domain/
│   ├── types/                  # 型定義(データのみ)
│   │   ├── order.py
│   │   ├── money.py
│   │   └── errors.py
│   ├── operations/             # 純粋関数(ロジック)
│   │   ├── order_ops.py
│   │   └── pricing_ops.py
│   └── ports/                  # 副作用のインターフェース
│       └── order_port.py

├── application/
│   └── workflows/              # ワークフロー(関数の合成)
│       └── create_order.py

├── infrastructure/
│   └── adapters/               # 副作用の実装
│       └── order_adapter.py

└── main.py

型定義(イミュータブルなデータ)

# domain/types/order.py
from dataclasses import dataclass
from typing import Tuple
from uuid import UUID
from enum import Enum

from domain.types.money import Money

class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    CANCELLED = "cancelled"

@dataclass(frozen=True)
class OrderItem:
    product_id: str
    product_name: str
    price: Money
    quantity: int

@dataclass(frozen=True)
class Order:
    id: UUID
    customer_id: UUID
    items: Tuple[OrderItem, ...]  # イミュータブルなtuple
    status: OrderStatus

エラー型の定義:

# domain/types/errors.py
from dataclasses import dataclass
from typing import Union

@dataclass(frozen=True)
class OrderError:
    message: str
    code: str

# エラー型の定義
EmptyOrderError = OrderError("商品がありません", "EMPTY_ORDER")
AlreadyConfirmedError = OrderError("既に確定済みです", "ALREADY_CONFIRMED")

# Result型(成功 or エラー)
type Result[T] = Union[T, OrderError]

純粋関数(ドメインロジック)

データを受け取り、新しいデータを返します。副作用なし。

# domain/operations/order_ops.py
from uuid import UUID, uuid4

from domain.types.order import Order, OrderItem, OrderStatus
from domain.types.money import Money
from domain.types.errors import Result, OrderError, EmptyOrderError, AlreadyConfirmedError

def create_order(customer_id: UUID) -> Order:
    """新しい注文を作成"""
    return Order(
        id=uuid4(),
        customer_id=customer_id,
        items=(),
        status=OrderStatus.PENDING,
    )

def add_item(order: Order, item: OrderItem) -> Result[Order]:
    """商品を追加(新しいOrderを返す)"""
    if order.status != OrderStatus.PENDING:
        return AlreadyConfirmedError

    return Order(
        id=order.id,
        customer_id=order.customer_id,
        items=order.items + (item,),
        status=order.status,
    )

def confirm(order: Order) -> Result[Order]:
    """注文を確定"""
    if not order.items:
        return EmptyOrderError
    if order.status != OrderStatus.PENDING:
        return AlreadyConfirmedError

    return Order(
        id=order.id,
        customer_id=order.customer_id,
        items=order.items,
        status=OrderStatus.CONFIRMED,
    )

def calculate_total(order: Order) -> Money:
    """合計金額を計算"""
    total = sum(item.price.amount * item.quantity for item in order.items)
    return Money(total)

OOPと関数型の違い

✅ 関数型スタイル

新しいOrderを返す(イミュータブル)

new_order = add_item(order, item)
# 元のorderは変更されない
❌ OOPスタイル

自分自身を変更する(ミュータブル)

order.add_item(item)
# orderが変更される

エラーハンドリングの違い

OOPでは例外を投げます:

class Order:
    def confirm(self) -> None:
        if not self.items:
            raise ValueError("商品がありません")
        self.status = OrderStatus.CONFIRMED

関数型では型でエラーを表現します:

def confirm(order: Order) -> Union[Order, OrderError]:
    if not order.items:
        return OrderError("商品がありません")
    return Order(
        id=order.id,
        items=order.items,
        status=OrderStatus.CONFIRMED,
    )

# 使用(エラーチェックが強制される)
result = confirm(order)
if isinstance(result, OrderError):
    print(result.message)
else:
    save(result)

関数型DDDの利点

テストが簡単(モック不要):

def test_add_item():
    order = create_order(uuid4())
    item = OrderItem("p1", "商品", Money(100), 1)

    result = add_item(order, item)

    assert isinstance(result, Order)
    assert len(result.items) == 1
    assert order.items == ()  # 元のorderは変更されない

def test_confirm_empty_order():
    order = create_order(uuid4())

    result = confirm(order)

    assert result == EmptyOrderError

OOP vs 関数型 DDDの比較

観点OOP DDD関数型 DDD
エンティティクラス(データ+メソッド)データ型 + 関数群
状態変更order.add_item(item)new_order = add_item(order, item)
エラー例外を投げるResult[T]型で返す
リポジトリ抽象クラス+実装クラスProtocol + 関数注入
ユースケースクラスのexecuteメソッド関数の合成
テストモック必要純粋関数は入出力のみ

どちらを選ぶ?

状況推奨
チームがOOPに慣れているOOP
並行処理が多い関数型
テストしやすさ重視関数型
Python/Java/C#OOP(言語の特性を活かす)
Haskell/Elixir/F#関数型
📌

実際にはハイブリッドも多く、イミュータブルなデータクラスとOOPを組み合わせるのが現実的です。

DDDの選定基準

DDDが向いているケース

DDDを採用すべき状況
  • 複雑なビジネスロジックがある
  • 長期間(数年以上)運用する
  • 頻繁に仕様変更がある
  • ドメインエキスパートと連携できる
  • チーム規模が中〜大(5人以上)
  • マイクロサービス化を検討している

具体例:

  • ECサイト(注文、在庫、配送、決済の複雑な連携)
  • 金融システム(取引、口座、規制対応)
  • 予約システム(空き状況、料金計算、キャンセルポリシー)
  • 医療システム(患者、診療、処方の複雑なルール)

DDDが向いていないケース

  • シンプルなCRUDアプリ
  • 短期プロジェクト(数ヶ月で終了)
  • 仕様が固定で変更が少ない
  • 1〜2人の小規模チーム
  • プロトタイプ・MVP
  • ドメインエキスパートがいない

判断フローチャート

ビジネスロジックは複雑?
├─ No → DDDは不要(シンプルなMVCで十分)
└─ Yes

長期運用(2年以上)?
├─ No → DDDは過剰(軽量なクリーンアーキテクチャ程度)
└─ Yes

ドメインエキスパートと連携可能?
├─ No → 戦術的DDDのみ採用(パターンだけ使う)
└─ Yes

チームにDDD経験者がいる?
├─ No → 学習期間を設けてから段階的に導入
└─ Yes → フルDDDを採用

段階的な採用戦略

レベル採用範囲適用ケース
Level 0なしシンプルなアプリ
Level 1値オブジェクトのみデータの型安全性を上げたい
Level 2エンティティ + リポジトリビジネスロジックを整理したい
Level 3集約 + ドメインサービス複雑なルールを管理したい
Level 4境界づけられたコンテキストマイクロサービス化
Level 5フルDDD + イベント駆動大規模エンタープライズ
💡 迷ったら

まず値オブジェクトとエンティティだけ導入し、必要に応じて拡張する「段階的採用」がおすすめです。

DDDのメリット・デメリット

✅ メリット

  • ビジネスロジックが整理される
  • 変更に強い設計になる
  • チーム間の認識が揃う
  • テストしやすい
  • 長期保守がしやすい

⚠️ デメリット

  • 学習コストが高い
  • 初期の開発速度が遅い
  • 小規模プロジェクトには過剰
  • ドメインエキスパートの協力が必要
  • 設計の合意形成に時間がかかる

代替・併用できるアーキテクチャ

アーキテクチャ複雑さ適用ケース
MVCシンプルなWebアプリ
クリーンアーキテクチャ依存関係の整理が必要
ヘキサゴナル外部依存を分離したい
DDD複雑なドメインロジック
イベント駆動非同期・分散システム

まとめ

DDDは複雑なビジネスロジックを持つシステムの設計に適した手法です。

質問DDDを採用
ビジネスルールを説明するのに30分以上かかる?
「この場合はどうなる?」の質問が多い?
仕様変更で広範囲のコード修正が発生する?
データベースのテーブル設計から始める?
ほぼCRUD操作だけ?

言語よりもチームの理解と設計方針が重要です。OOPスタイルと関数型スタイルのどちらを選ぶかは、チームの経験や要件に応じて判断してください。

参考文献

CATEGORY
TAGS
円