アプリケーションを開発していると、いつの間にかコードがごちゃごちゃになっている、なんて経験はありませんか?
ビジネスロジックやデータベース、外部サービスへの接続コードが一箇所に混ざり合い、変更を加えるたびに「あれ、どこを直せばいいんだっけ?」と戸惑う――こんな状況は、多くの開発者が通る道です。
ここで頼りになるのが、ドメイン駆動開発(DDD)とクリーンアーキテクチャの考え方です。どちらも「重要なビジネスロジックを明確にし、外部の技術的要素から切り離す」ことを目指しており、その組み合わせはコードベースの見通しを劇的に改善します。本記事では、これらの考え方をふまえたPythonプロジェクトのフォルダ構成例を紹介します。
「次の機能追加はどうレイヤーに分けよう?」「DBを変えてもビジネスロジックに影響が出ないようにしたい」――そんなときに立ち戻れる、一つの指針として参考にしてください。
なぜDDDとクリーンアーキテクチャを組み合わせるのか?
- DDD (ドメイン駆動開発):ソフトウェアの「本当に重要な部分」、すなわちビジネスロジック(ドメイン)を中心に据え、チームが理解しやすいモデルとして定義していく手法です。
- クリーンアーキテクチャ:コアのロジックを外部技術から独立させ、依存関係を内側から外側へ一方向に保ちます。これにより、UI、DB、フレームワークなどを取り替えても、ビジネスロジックが簡単には揺らぎません。
両者を組み合わせることで、ドメインモデルを中心とした明確な構造をつくりやすくなります。ドメインをしっかり定義し、その周囲にアプリケーション層、インフラ層、インターフェース層を配していくことで、どこに何を書くべきかが自然に決まってきます。
Pythonプロジェクトの基本スタイル
最近のPythonプロジェクトでは、src/ 配下にコードをまとめ、pyproject.tomlで依存管理をするパターンがよく使われます。PoetryやPipenvとの相性も良く、CI/CDパイプライン構築もやりやすいのがメリットです。
ここで紹介する分割は大きく4つのレイヤーです。
- domain:ビジネスロジックの本体(エンティティ、値オブジェクト、ドメインサービス、リポジトリの抽象インターフェース)
- application:ユースケース(ビジネスロジックを活用して、アプリケーションとしての操作・振る舞いを定義)
- infrastructure:DBや外部APIアクセスなど、環境依存の実装部分
- interface:WebフレームワークやCLIなど、ユーザーや外部からの入力・出力部分
フォルダ構成例
以下は一つの参考例です。プロジェクト規模や要件に合わせてアレンジできますが、この構成を押さえておくと迷いにくくなります。
project_name/
├─ pyproject.toml
├─ README.md
├─ docs/
├─ tests/
└─ src/
├─ domain/
│ ├─ entities/
│ │ ├─ user.py
│ │ └─ order.py
│ ├─ value_objects/
│ │ └─ email_address.py
│ ├─ services/
│ │ └─ order_service.py
│ └─ repositories/
│ └─ user_repository.py # 抽象インターフェースのみ定義
│
├─ application/
│ ├─ use_cases/
│ │ ├─ create_user.py
│ │ ├─ get_user.py
│ │ └─ update_order.py
│ ├─ dtos/
│ │ └─ user_dto.py
│ └─ interfaces/
│ └─ user_repository_if.py # domainの抽象Repoを参照
│
├─ infrastructure/
│ ├─ persistence/
│ │ ├─ orm_user.py # DB接続によるUserRepo実装
│ │ └─ db_config.py
│ ├─ external_services/
│ │ └─ payment_gateway.py
│ └─ config/
│ └─ settings.py
│
└─ interface/
├─ web/
│ ├─ fastapi_app.py
│ ├─ routes.py
│ └─ controllers/
│ └─ user_controller.py
├─ cli/
│ └─ cli_commands.py
└─ presenters/
└─ user_presenter.py
各ディレクトリの役割
- domain/:ビジネスモデルが純度100%で存在する場所。外部には依存せず、エンティティや値オブジェクトを定義します。
- application/:ユースケース単位でビジネスロジックを呼び出す層。domainには依存しますが、infrastructureやinterfaceには直接依存しないようにします。
- infrastructure/:具体的なDBアクセスや外部サービスへの接続処理など、技術的な詳細を実装する層です。
- interface/:Web APIやCLIなど、外部からアプリケーションを操作する際の入り口。コントローラやルーティング、プレゼンテーション周りの処理をまとめます。
依存関係の流れ
クリーンアーキテクチャでは、ドメインが一番内側に位置します。
- domainはどこにも依存しない
- applicationはdomainへ依存するが、infrastructureやinterfaceには依存しない(抽象経由で接続)
- infrastructureとinterfaceはapplicationやdomainの抽象を実装し、外側から内側を参照しない
こうすることで、たとえばDBを変えたくなったときでもdomainやapplicationには手を入れず、infrastructure側だけで対応できます。テスト時にはMockリポジトリを簡単に差し替えることも容易です。
簡単な例:UserエンティティとUseCase
domain層
# src/domain/entities/user.py
class User:
def __init__(self, user_id: str, email: "EmailAddress"):
self.user_id = user_id
self.email = email
# src/domain/value_objects/email_address.py
class EmailAddress:
def __init__(self, value: str):
# メール形式の簡易チェックなど
self.value = value
# src/domain/repositories/user_repository.py
from abc import ABC, abstractmethod
from domain.entities.user import User
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: str) -> User:
pass
@abstractmethod
def save(self, user: User) -> None:
pass
application層
# src/application/use_cases/create_user.py
from domain.repositories.user_repository import UserRepository
from domain.value_objects.email_address import EmailAddress
from domain.entities.user import User
class CreateUserUseCase:
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
def execute(self, user_id: str, email_str: str):
email = EmailAddress(email_str)
user = User(user_id, email)
self.user_repo.save(user)
infrastructure層(DB実装例)
# src/infrastructure/persistence/orm_user.py
from domain.repositories.user_repository import UserRepository
from domain.entities.user import User
class ORMUserRepository(UserRepository):
def find_by_id(self, user_id: str) -> User:
# DB検索処理(ORMによる実装)
...
def save(self, user: User) -> None:
# DBへINSERT/UPDATE
...
interface層(Webエンドポイント例)
# src/interface/web/user_controller.py
from fastapi import APIRouter
from application.use_cases.create_user import CreateUserUseCase
from infrastructure.persistence.orm_user import ORMUserRepository
router = APIRouter()
@router.post("/users")
def create_user(user_id: str, email: str):
uc = CreateUserUseCase(ORMUserRepository())
uc.execute(user_id, email)
return {"status": "ok"}
このようにドメインやアプリケーション層では具体的なORMやWebフレームワークに依存していないため、外側の技術を置き換えるのも比較的容易です。
まとめ
- DDDでビジネスロジック(ドメイン)をしっかり定義
- クリーンアーキテクチャで依存関係を整理してテストしやすいコード構成へ
- Pythonで src/ 以下にレイヤー分割すると、変更容易性・保守性が大幅に向上
この構成を導入すれば、「またDBを変えるの?」「フレームワークをアップデートするの面倒だな」といった局面でも、コアなビジネスロジックは手を加えずに済むはずです。日々の開発で悩むことが少しでも減り、あなたのチームがよりスムーズにプロダクトを成長させていくためのヒントになれば幸いです。