DDD (ドメイン駆動設計) アーキテクチャ¶
概要¶
本プロジェクトは、ドメイン駆動設計(DDD)をベースにしたクリーンアーキテクチャを採用しています。これにより、ビジネスロジックの純粋性を保ちながら、技術的な詳細から分離された保守性の高いシステムを実現します。
アーキテクチャの全体像¶
graph TD
subgraph Interfaces層 [Interfaces 層(FastAPI)]
A1[REST API / GraphQL / CLI]
A2[Request DTO / Response DTO]
end
subgraph Application層 [Application 層(ユースケース)]
B1[UserSummaryUseCase]
B2[複数DB統合ロジック]
end
subgraph Domain層 [Domain 層(ビジネスルール)]
C1[Entity: UserProfile]
C2[Value Object]
C3[Domain Service]
end
subgraph Infrastructure層 [Infrastructure 層(技術詳細)]
D1[PostgreSQL Repository]
D2[Neo4j Repository]
D3[External API Client]
end
A1 --> A2
A2 --> B1
B1 --> C1
B1 --> D1
B1 --> D2
D1 --> C1
D2 --> C1
依存関係の方向¶
graph LR
Interfaces --> Application
Application --> Domain
Infrastructure --> Domain
Interfaces -.-> Infrastructure
style Domain fill:#90EE90
style Application fill:#87CEEB
style Infrastructure fill:#FFB6C1
style Interfaces fill:#FFD700
依存の原則¶
- 内向き依存: すべての依存は内側(Domain層)に向かう
- Domain層: 外部への依存なし(純粋なビジネスロジック)
- Application層: Domainに依存、Infrastructureは抽象に依存
- Infrastructure層: Domainの抽象(Repository Interface)に依存
- Interfaces層: Applicationを呼び出し、Infrastructureを注入
各層の責務¶
1. Domain層(ドメイン層)¶
責務: ビジネスルールとビジネスロジックの中核
含まれるもの: - Entity(エンティティ): ビジネス上の重要な概念(例: User, Order) - Value Object(値オブジェクト): 不変の値(例: Email, Money) - Domain Service(ドメインサービス): 複数エンティティにまたがるビジネスロジック - Repository Interface(リポジトリインターフェイス): データ永続化の抽象
特徴: - 外部ライブラリへの依存なし(pure Python) - フレームワーク非依存 - テストが最も容易
実装例:
# app/domain/user.py
from dataclasses import dataclass
from datetime import datetime
@dataclass
class UserProfile:
"""ユーザープロファイルエンティティ"""
id: int
name: str
email: str
joined_at: datetime
def is_premium_user(self) -> bool:
"""プレミアムユーザー判定ロジック"""
days_since_joined = (datetime.now() - self.joined_at).days
return days_since_joined > 30
def to_dict(self) -> dict:
"""辞書形式に変換"""
return {
"id": self.id,
"name": self.name,
"email": self.email,
"joined_at": self.joined_at.isoformat(),
"is_premium": self.is_premium_user()
}
# app/domain/repository_interface.py
from abc import ABC, abstractmethod
from typing import Optional
from app.domain.user import UserProfile
class UserRepositoryInterface(ABC):
"""ユーザーリポジトリの抽象"""
@abstractmethod
def find_by_id(self, user_id: int) -> Optional[UserProfile]:
pass
@abstractmethod
def save(self, user: UserProfile) -> UserProfile:
pass
2. Application層(アプリケーション層・ユースケース層)¶
責務: ビジネスユースケースの調整・統合
含まれるもの: - UseCase(ユースケース): ビジネスシナリオの実装 - 複数DBの統合ロジック: 異なるリポジトリからのデータ結合 - トランザクション管理: 複数の操作をまとめる
特徴: - Domainのエンティティとサービスを組み合わせる - Repositoryインターフェイスを通じてデータアクセス - ビジネスフローの調整役
実装例:
# app/application/user_summary_usecase.py
from app.domain.user import UserProfile
from app.domain.repository_interface import UserRepositoryInterface
class UserSummaryUseCase:
"""ユーザーサマリー取得ユースケース"""
def __init__(
self,
corporate_repo: UserRepositoryInterface,
auth_repo: UserRepositoryInterface
):
self.corporate_repo = corporate_repo
self.auth_repo = auth_repo
def execute(self, user_id: int) -> dict:
"""
複数DBからユーザー情報を統合して返す
Args:
user_id: ユーザーID
Returns:
統合されたユーザー情報
"""
# corporateDBからプロファイル取得
profile = self.corporate_repo.find_by_id(user_id)
if not profile:
raise ValueError(f"User {user_id} not found")
# auth_sharedDBから認証情報取得
auth_info = self.auth_repo.find_by_id(user_id)
# 統合してJSONに変換
return {
"user_id": profile.id,
"name": profile.name,
"email": auth_info.email if auth_info else profile.email,
"joined_at": profile.joined_at.isoformat(),
"is_premium": profile.is_premium_user()
}
3. Infrastructure層(インフラストラクチャ層)¶
責務: 技術的な詳細の実装
含まれるもの: - Repository実装: DB接続の具体的実装 - 外部API Client: 外部サービスとの通信 - ORM Model: SQLAlchemyなどのORMモデル
特徴: - Domainのインターフェイスを実装 - DB接続先ごとに分離(corporate / miniapp_lab / auth_shared) - 技術スタックの変更が容易
マルチDB構成:
infrastructure/
├─ corporate/
│ ├─ repository.py # corporate DB用リポジトリ実装
│ ├─ orm_model.py # corporate DB用ORMモデル
│ └─ connection.py # corporate DB接続設定
├─ miniapp_lab/
│ ├─ repository.py
│ ├─ orm_model.py
│ └─ connection.py
├─ auth_shared/
│ ├─ repository.py
│ ├─ orm_model.py
│ └─ connection.py
└─ vector/
├─ pgvector_repository.py # ベクトル検索用
└─ vector_model.py
実装例:
# app/infrastructure/corporate/repository.py
from sqlalchemy.orm import Session
from app.domain.user import UserProfile
from app.domain.repository_interface import UserRepositoryInterface
from app.infrastructure.corporate.orm_model import UserORM
class CorporateUserRepository(UserRepositoryInterface):
"""Corporate DB用ユーザーリポジトリ実装"""
def __init__(self, session: Session):
self.session = session
def find_by_id(self, user_id: int) -> Optional[UserProfile]:
"""IDでユーザーを取得"""
user_orm = self.session.query(UserORM).filter(
UserORM.id == user_id
).first()
if not user_orm:
return None
# ORMモデルからドメインエンティティに変換
return UserProfile(
id=user_orm.id,
name=user_orm.name,
email=user_orm.email,
joined_at=user_orm.created_at
)
def save(self, user: UserProfile) -> UserProfile:
"""ユーザーを保存"""
user_orm = UserORM(
id=user.id,
name=user.name,
email=user.email,
created_at=user.joined_at
)
self.session.add(user_orm)
self.session.commit()
return user
# app/infrastructure/corporate/orm_model.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class UserORM(Base):
"""Corporate DB用ユーザーORMモデル"""
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
email = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False)
4. Interfaces層(インターフェイス層)¶
責務: 外部とのやり取り(入出力)
含まれるもの: - API Router: FastAPIのエンドポイント定義 - DTO (Data Transfer Object): リクエスト/レスポンスの型定義 - Dependency Injection: 依存性注入の設定
特徴: - Applicationレイヤーのユースケースを呼び出す - HTTPリクエストをDTOに変換 - Pydanticでバリデーション
実装例:
# app/interfaces/routers/user_router.py
from fastapi import APIRouter, Depends
from app.application.user_summary_usecase import UserSummaryUseCase
from app.interfaces.dependencies import get_user_summary_usecase
from app.interfaces.schemas.user_schema import UserSummaryResponse
router = APIRouter(prefix="/api/users", tags=["users"])
@router.get("/{user_id}", response_model=UserSummaryResponse)
async def get_user_summary(
user_id: int,
usecase: UserSummaryUseCase = Depends(get_user_summary_usecase)
):
"""
ユーザーサマリーを取得
複数DBから情報を統合して返します
"""
result = usecase.execute(user_id)
return result
# app/interfaces/schemas/user_schema.py
from pydantic import BaseModel
from datetime import datetime
class UserSummaryResponse(BaseModel):
"""ユーザーサマリーレスポンス"""
user_id: int
name: str
email: str
joined_at: str
is_premium: bool
class Config:
json_schema_extra = {
"example": {
"user_id": 1,
"name": "田中太郎",
"email": "tanaka@example.com",
"joined_at": "2024-01-01T00:00:00",
"is_premium": True
}
}
# app/interfaces/dependencies.py
from fastapi import Depends
from sqlalchemy.orm import Session
from app.application.user_summary_usecase import UserSummaryUseCase
from app.infrastructure.corporate.repository import CorporateUserRepository
from app.infrastructure.auth_shared.repository import AuthUserRepository
from app.core.database import get_corporate_db, get_auth_db
def get_user_summary_usecase(
corporate_db: Session = Depends(get_corporate_db),
auth_db: Session = Depends(get_auth_db)
) -> UserSummaryUseCase:
"""ユースケースの依存性注入"""
corporate_repo = CorporateUserRepository(corporate_db)
auth_repo = AuthUserRepository(auth_db)
return UserSummaryUseCase(corporate_repo, auth_repo)
データフロー(シーケンス図)¶
sequenceDiagram
participant UI as FastAPI Controller (Interfaces)
participant UC as UseCase (Application)
participant DM as Domain Entity
participant RP1 as CorporateRepository (Infrastructure)
participant RP2 as AuthRepository (Infrastructure)
participant DB1 as PostgreSQL (corporate)
participant DB2 as PostgreSQL (auth_shared)
UI->>UC: リクエスト受信 (user_id)
UC->>RP1: find_by_id(user_id)
RP1->>DB1: SQL実行
DB1-->>RP1: データ返却
RP1->>DM: ORMからEntityに変換
DM-->>UC: UserProfile返却
UC->>RP2: find_by_id(user_id)
RP2->>DB2: SQL実行
DB2-->>RP2: データ返却
RP2->>DM: ORMからEntityに変換
DM-->>UC: AuthInfo返却
UC->>UC: データ統合・ビジネスロジック適用
UC-->>UI: 統合結果を返却 (JSON)
UI-->>Client: HTTPレスポンス
マルチデータベース統合パターン¶
3つのデータベース構成¶
graph TB
subgraph Application Layer
UC[UserSummaryUseCase]
end
subgraph Infrastructure Layer
R1[CorporateRepository]
R2[MiniAppRepository]
R3[AuthRepository]
end
subgraph Database Layer
DB1[(PostgreSQL<br/>corporate)]
DB2[(PostgreSQL<br/>miniapp_lab)]
DB3[(PostgreSQL<br/>auth_shared)]
end
UC --> R1
UC --> R2
UC --> R3
R1 --> DB1
R2 --> DB2
R3 --> DB3
style UC fill:#87CEEB
style R1 fill:#FFB6C1
style R2 fill:#FFB6C1
style R3 fill:#FFB6C1
style DB1 fill:#98FB98
style DB2 fill:#98FB98
style DB3 fill:#98FB98
データベース別の役割¶
| データベース | 用途 | 主なテーブル |
|---|---|---|
| corporate | コーポレートサイト用データ | users, posts, pages |
| miniapp_lab | 小規模実験サービス | experiments, prototypes |
| auth_shared | 認証・ユーザー情報共通化 | auth_users, sessions, permissions |
統合処理の責務分離¶
| 処理内容 | 責務を持つ層 | 理由 |
|---|---|---|
| DBごとのデータ取得 | Infrastructure層(Repository) | 永続化層の責務 |
| 取得したデータの結合や整形 | Application層(UseCase) | ユースケース固有の組み合わせロジック |
| ビジネスルールの適用 | Domain層(Entity/Service) | ビジネスロジックの純粋性保持 |
| JSONへの変換 | Application層 → Interfaces層 | 出力形式の制御 |
テスト戦略¶
レイヤー別のテストアプローチ¶
graph LR
subgraph テスト種別
T1[Unit Test<br/>ユニットテスト]
T2[Integration Test<br/>統合テスト]
T3[E2E Test<br/>エンドツーエンドテスト]
end
subgraph 対象レイヤー
L1[Domain層]
L2[Application層]
L3[Infrastructure層]
L4[Interfaces層]
end
T1 --> L1
T1 --> L2
T2 --> L3
T3 --> L4
| テスト対象 | テスト種類 | 概要 | ツール |
|---|---|---|---|
| Domain層 | ユニットテスト | pure Python。外部依存なし | pytest |
| Application層 | ユースケーステスト | モックリポジトリでDBを外す | pytest + unittest.mock |
| Infrastructure層 | 統合テスト | Dockerコンテナ上で実DB接続 | pytest + testcontainers |
| Interfaces層 | APIテスト | FastAPIのTestClientを使用 | pytest + TestClient |
TDD(テスト駆動開発)の流れ¶
graph TD
A[1. ユースケーステストを書く] --> B[2. Domainエンティティ設計]
B --> C[3. Repositoryインターフェイス定義]
C --> D[4. モックでテスト実行]
D --> E{テスト成功?}
E -->|No| B
E -->|Yes| F[5. Infrastructure実装]
F --> G[6. 統合テスト実行]
G --> H{テスト成功?}
H -->|No| F
H -->|Yes| I[7. Interfaces層実装]
I --> J[8. E2Eテスト実行]
J --> K[完了]
ディレクトリ構成¶
project-root/
├─ app/
│ ├─ main.py # FastAPI起動
│ ├─ core/ # 共通設定
│ │ ├─ config.py # 設定管理
│ │ ├─ database.py # DB接続管理
│ │ └─ logging.py # ログ設定
│ ├─ domain/ # ドメイン層
│ │ ├─ user.py # Userエンティティ
│ │ ├─ value_objects.py # 値オブジェクト
│ │ ├─ domain_service.py # ドメインサービス
│ │ └─ repository_interface.py # リポジトリIF
│ ├─ application/ # アプリケーション層
│ │ ├─ user_summary_usecase.py
│ │ └─ create_user_usecase.py
│ ├─ infrastructure/ # インフラストラクチャ層
│ │ ├─ corporate/ # corporate DB用
│ │ │ ├─ repository.py
│ │ │ ├─ orm_model.py
│ │ │ └─ connection.py
│ │ ├─ miniapp_lab/ # miniapp DB用
│ │ │ ├─ repository.py
│ │ │ └─ orm_model.py
│ │ ├─ auth_shared/ # auth DB用
│ │ │ ├─ repository.py
│ │ │ └─ orm_model.py
│ │ └─ vector/ # pgvector用
│ │ ├─ repository.py
│ │ └─ model.py
│ ├─ interfaces/ # インターフェイス層
│ │ ├─ routers/ # APIルーター
│ │ │ ├─ user_router.py
│ │ │ └─ health_router.py
│ │ ├─ schemas/ # Pydantic DTO
│ │ │ ├─ user_schema.py
│ │ │ └─ response_schema.py
│ │ └─ dependencies.py # DI設定
│ └─ tests/ # テスト
│ ├─ unit/ # ユニットテスト
│ ├─ integration/ # 統合テスト
│ └─ e2e/ # E2Eテスト
├─ docs/ # ドキュメント
├─ docker-compose.yaml
└─ .env
まとめ¶
設計原則¶
| 項目 | 内容 |
|---|---|
| 設計思想 | DDDをベースにしたクリーンアーキテクチャ |
| 依存方向 | 内向き(Domainが最も純粋) |
| 責務分離 | Router=入出力、Application=統合、Domain=ビジネス |
| DB複数接続 | Infrastructureで分離、Applicationで統合 |
| テスト | Domain=ユニット、Application=モック、Infrastructure=統合テスト |
利点¶
- ビジネスロジックの純粋性: Domainが外部依存を持たない
- テスタビリティ: 各層を独立してテスト可能
- 保守性: 責務が明確で変更の影響範囲が限定的
- 拡張性: 新しいDBやサービスの追加が容易
- 技術スタックの変更: InfrastructureとInterfacesを変えるだけ
適用シーン¶
- 長期運用が想定されるプロジェクト
- チーム開発(責務分離により並行開発が容易)
- 複雑なビジネスルールを持つシステム
- 複数のデータソースを統合する必要がある場合
- テストカバレッジを高く保ちたい場合