フクロウと学ぶアーキテクチャ #2 ─ ドメイン中心アーキテクチャ入門(Clean / Hexagonal / DDD)
※本記事は個人の知見と学習の記録であり、所属組織の見解を代表するものではありません。
🦉フクロウと学ぶアーキテクチャ #2
ドメイン中心アーキテクチャ入門 ─ Clean / Hexagonal / DDD を「タスク管理アプリ」で体験する
ソフトウェアアーキテクチャの旅、その 2ページ目 は
ドメイン中心アーキテクチャ(Domain-Centric Architecture) です。
前回に続き、今回も「迷子にならないための地図」を一緒に描いていきます。
前回は レイヤードアーキテクチャ(N-Tier) を題材に、
- UI
- サービス(ビジネスロジック)
- データアクセス
という「技術ごとの棚」でコードを整理する設計を見てきました。
今回はその“次の一歩”として、
技術よりもドメイン(ビジネスの意味そのもの)を中心に据える設計──
- Clean Architecture
- Hexagonal Architecture(Ports & Adapters)
- DDD(Domain-Driven Design)
の 共通エッセンス(内側を守る構造) を、
今回は タスク管理 CLI アプリを通して体験します。
※本サンプルは Clean/Hexagonal をベースにしており、DDD の戦術パターン(集約・仕様・ドメインサービスなど)は最小限に抑えた“DDD 入門向け”の軽量版です。

なぜ「レイヤードの次」がドメイン中心なのか?
レイヤードは歴史が長く、今でも多くのフレームワークで
「デフォルトの地図」として採用される構造です。
しかしモバイルアプリ、Web API、Webhook、エージェント、外部 SaaS など
“入口(UI)”が爆発的に増える現代では、レイヤード構造は次のような壁にぶつかります:
- UI が複数になると Service 層が太りがち
- DB が複数になると Repository が複雑化
- 外部サービス(SaaS)が増えると境界が曖昧化
ドメイン中心アーキテクチャは、この「外側の増加と複雑化」から
内側(ドメインの“意味”)を守り抜くために登場した考え方です。
“変わりやすい外側”と“変わってほしくない中心”を切り離すための地図、と言ってもよいでしょう。

今回めざすゴール
- 「ドメイン中心アーキテクチャって、ざっくり何?」がつかめる
- Clean / Hexagonal / DDD が 何を守ろうとしているのか が理解できる
- GitHub のサンプルコードをいじりながら、
「ドメインを変えても外側は壊れない」
「外側を変えてもドメインはそのまま」
を実感できる
Clean / Hexagonal / DDD をざっくり整理しておく
ここから先を読みやすくするために、3つの名前を先にざっくりそろえておきます。
-
Clean Architecture
- 内側ほど「ビジネスのルール」、外側ほど「技術的な都合」という入れ子構造
- 依存は必ず「外側 → 内側」に向かう というルールが中心
-
Hexagonal Architecture(Ports & Adapters)
- ドメイン側から見た「お願い窓口(Port)」と、その実装である「Adapter」に分ける考え方
- UI や DB、外部 SaaS など、外の世界を Port/Adapter 経由で差し替え可能にする
-
DDD(Domain-Driven Design)
- ドメインの専門家と同じ言葉でモデルをつくり、
コードと会話の言葉をそろえていくプラクティスの集合 - 戦術パターン(Entity, ValueObject など)や戦略パターン(境界づけられたコンテキストなど)がある
- ドメインの専門家と同じ言葉でモデルをつくり、
今回のサンプルコードは、Clean / Hexagonal の構造をベースにしつつ、DDD の「ドメインの意味を中心に置く」という部分だけを軽量に取り入れたもの と捉えてもらえるとイメージしやすいと思います。
pythonで体験するdomain-centric
サンプルの全体像 ─ core が中心、adapters が外側
今回のサンプルはこちらです:
👉 samples/domain-centric/simple-cli
構成は次のようになっています。
src/
main.py # コンポジションルート(依存の組み立て)
core/ # アプリケーションの中心(ドメイン+ユースケース)
entities.py # ドメインモデル(Task など)
ports.py # ポート(インターフェース)=ドメイン視点の依存先
use_cases.py # ユースケース(アプリケーションサービス)
adapters/ # 外側の世界(入出力・永続化など)
in_memory_task_repository.py # ポートを実装したインメモリリポジトリ
cli/
controller.py # CLI からユースケースを呼び出すアダプタ
この構成でいちばん大事なのは 依存の向き です。

-
core(ドメイン・ユースケース)は 何にも依存しない -
adaptersは core に依存する -
main.pyは 依存を組み立てるだけ
つまり、
依存は常に “外側 → 内側” に向かう。
内側(ドメイン)は、外側の都合や技術変更から守られる。
core:アプリケーションの中心(ドメイン+ユースケース)
🔰 この記事での core の意味
前回の “domain 層 + service 層” の役割を、
「ビジネス(entities)+ユースケース(use_cases)」 に再構成したものです。
1. ドメインモデル(entities.py)
まずは、このアプリケーションが扱いたい「モノ」を定義します。
今回はシンプルに タスク管理 を扱うことにします。
from dataclasses import dataclass
@dataclass
class Task:
id: int
title: str
is_completed: bool = False
def complete(self) -> None:
self.is_completed = True
ここに書かれているのは、
- タスクとは何か(id, title, 状態)
- タスクの振る舞い(complete)
だけであり、
❌ DB
❌ CLI
❌ ファイル保存
といった技術要素は一切入りません。
2. ポート(ports.py)─ ドメインから見た「お願い」

次に、「タスクを扱ううえで、外側に何をお願いしたいか」を定義します。
“お願いの窓口”が Port(ポート) です。
from abc import ABC, abstractmethod
from typing import List, Optional
from .entities import Task
class TaskRepository(ABC):
@abstractmethod
def next_identity(self) -> int:
...
@abstractmethod
def save(self, task: Task) -> None:
...
@abstractmethod
def list_all(self) -> List[Task]:
...
@abstractmethod
def find_by_id(self, task_id: int) -> Optional[Task]:
...
ここで定義しているのは 「やってほしいこと」だけ。
3. ユースケース(use_cases.py)─ アプリの“用事”
from dataclasses import dataclass
from .entities import Task
from .ports import TaskRepository
@dataclass
class AddTaskUseCase:
repository: TaskRepository
def execute(self, title: str) -> Task:
new_id = self.repository.next_identity()
task = Task(id=new_id, title=title)
self.repository.save(task)
return task
他にも、
- List(一覧)
- Complete(完了)
が定義されています。
ここでのポイントは、
- 具体的な実装(メモリか DB か)は知らない
-
TaskRepositoryという Port(インターフェース) に対して命令しているだけ
という点です。
ユースケースは“1つの用事を表すクラス”
として、ビジネスの流れを整理する役割を持ちます。
この「タスク追加」の用事が、CLI からどのように core と外側を行き来するかをシーケンス図で眺めてみます。
CLI はあくまで「タイトルを渡して実行するだけ」で、保存の詳細や ID 採番の方法は Port 経由で外側の Adapter に委ねられている、という構造が見えると思います。
adapters:外の世界(永続化・CLI)
adapters は 前回の “presentation + infrastructure” の外側部分を統合した領域です。
UI と永続化はどちらも「core の外」にあるため、ドメイン中心では同じ箱に入ります。
4. 永続化 Adapter(InMemoryTaskRepository)
from typing import Dict, List, Optional
from core.entities import Task
from core.ports import TaskRepository
class InMemoryTaskRepository(TaskRepository):
def __init__(self) -> None:
self._tasks: Dict[int, Task] = {}
self._current_id: int = 0
def next_identity(self) -> int:
self._current_id += 1
return self._current_id
def save(self, task: Task) -> None:
self._tasks[task.id] = task
def list_all(self) -> List[Task]:
return list(self._tasks.values())
def find_by_id(self, task_id: int) -> Optional[Task]:
return self._tasks.get(task_id)
後で DB 版に差し替えても、core は一切変更しなくてOK。
5. CLI Adapter(TaskCLIController)
class TaskCLIController:
def run(self) -> None:
while True:
self._print_menu()
choice = input("番号を選んでください: ").strip()
...
UI 側の Adapter が TaskCLIController です。
ここは“人が触る部分”を専門に担当します。
責務は UI のみに限定されています。
main.py:依存を“組み立てるだけ”の場所
from adapters.in_memory_task_repository import InMemoryTaskRepository
from adapters.cli.controller import TaskCLIController
from core.use_cases import AddTaskUseCase, ListTasksUseCase, CompleteTaskUseCase
def main() -> None:
repo = InMemoryTaskRepository()
add = AddTaskUseCase(repo)
list_ = ListTasksUseCase(repo)
complete = CompleteTaskUseCase(repo)
controller = TaskCLIController(add, list_, complete)
controller.run()
ここが Clean Architecture でいうコンポジションルートです。
“アプリの最後の 1 手”をまとめる場所、と覚えると理解しやすくなります。
依存注入の流れをシーケンス図で見ると、main.py が「どの順番でオブジェクトを組み立てているか」もイメージしやすくなります。
main.py 自体はビジネスロジックを一切持たず、「依存を外側から内側へとつないで、最後に CLI を走らせるだけ」という役割に絞られていることが伝わるはずです。
DeepDive
レイヤード版との違い(おさらい)
レイヤード:技術ごとに棚を分ける構造
UI
サービス
リポジトリ
DB
ドメイン中心:ドメイン(core) → ユースケース(core) → Adapter(外側)
core
adapters
main
最大の違いはとてもシンプルで、この一文に集約できます。
ドメインは外側を知らない。
(外側がドメインに合わせる)
これが Clean / Hexagonal / DDD に共通する大事な思想です。
ドメイン中心アーキテクチャのメリット / デメリット
前回と同じ形式で、初心者向けに整理すると次のとおりです。
| 項目 | メリット | デメリット |
|---|---|---|
| UI / Adapter 追加 | CLI → Web → Agent など容易 | Adapter 実装が増える |
| 永続化変更 | InMemory → DB へ差し替え容易 | 抽象化(Port)が必要 |
| テスト容易性 | core を単体テストしやすい | 設計に慣れが必要 |
| 責務の分離 | core が汚れない | 小規模アプリはやりすぎ感 |
遊んでみよう(学びを深める小ネタ)
- タスクに「締切日」や「優先度」を追加してみる
- タイトルが空文字ならエラーにしてみる
- リポジトリをファイル保存版に差し替えてみる
- ユースケースだけテストして動くか確認する
「core を変えても外側は壊れないか?」
「外側を変えても core はそのまま動くか?」
を意識すると理解が深まります。
まとめ
- core(ドメイン+ユースケース)は技術変更に影響されない
- adapters はいくつでも増やせる(CLI → Web → Agent)
- Clean / Hexagonal / DDD は “ドメイン(アプリの意味)を外側の変化から守って長生きさせる” という共通目的を持つ
- AI エージェント時代のように外の技術が激しく変わる時代ほど
ドメイン中心アーキテクチャが効いてくる
このシンプル CLI を触りながら、
レイヤードとは少し違う「設計の地図」を体で感じてもらえたら嬉しいです 🦉

次回
この連載では、最初に レイヤード と ドメイン中心 という
「1つのアプリケーションの中の構造」を見てきました。
次はそこから一歩外に出て、
に足を踏み入れていく予定です。
- どこまでを 1 システムとみなすのか
- どこからを別サービスとして切り出すのか
- 分けすぎたとき、分けなさすぎたとき、何が起こるのか
といったテーマを、またフクロウと一緒にゆるく旅していきます。
Discussion