ヘキサゴナルアーキテクチャは、アプリケーションの中核を UI やデータベースから切り離し、外部とのやり取りをすべて「ポート」と「アダプター」経由にする設計です。提唱者の Alistair Cockburn は正式名を Ports and Adapters とし、ヘキサゴナル(六角形)は別名として扱っています。ヘキサゴンアーキテクチャと呼ばれることもありますが同じものです。
この記事では、2005年の原典に書かれている定義と六角形の意味を確認したうえで、クリーンアーキテクチャ・オニオンアーキテクチャとの違い、Python での最小実装、依存方向をツールで守らせる方法、そして採用しない方がよい場面までを扱います。
まとめ:ヘキサゴナルアーキテクチャの要点
- 原典は Cockburn が2005年に発表した技術レポート(公開版は2005年9月4日付の v0.9)。目的は、アプリケーションを人・プログラム・自動テスト・バッチのどれからでも同じように動かせ、実際の DB や機器が無くても開発とテストができる状態にすることです。
- 六角形の「6」に意味はありません。ポートとアダプターを描き足す余白を取るための形です。ポートの数に規則は無く、Cockburn 個人は2〜4本を好むと書いています。
- ポートは、外からアプリケーションを動かす駆動側(primary / driving)と、アプリケーションが外を呼ぶ被駆動側(secondary / driven)に分かれます。
- クリーンアーキテクチャ(2012年)とオニオンアーキテクチャ(2008年)は、ヘキサゴナルと同じく依存を内側へ向けますが、内側の層構造まで決めている点が違います。ヘキサゴナルは内側と外側の区別しか決めません。
- 依存方向は放っておくと崩れます。Python なら import-linter、Java なら ArchUnit のような依存検査を CI に入れて守らせるのが実務上の要点です。
- ロジックがほとんど無い CRUD や短命なプロトタイプでは、ポートを切る手間が見返りを上回ります。
ヘキサゴナルアーキテクチャの定義と原典
Cockburn の原典が掲げる目的
原典は Humans and Technology の技術レポート「HaT Technical Report 2005.02」で、公開されている版は2005年9月4日付、読者のコメントを受けて改訂する前提の v0.9 です。パターン名は Ports and Adapters、分類は Object Structural、別名が Hexagonal Architecture と明記されています。
目的(Intent)は次の一文です。
Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.
人・プログラム・自動テストのどこから動かしても同じように振る舞い、本番の DB や機器が揃う前から開発とテストを進められることが狙いです。原典の冒頭は、UI も DB も無しで動くように作る効用として、自動回帰テスト、DB 停止時の継続動作、人手を介さないアプリケーション連携を挙げています。
当時 Cockburn が問題にしていたのは、ビジネスロジックが画面の実装に入り込み、フィールドの幅やボタンの配置が変わるたびにテストが壊れる状況でした。その後 Cockburn は Juan Manuel Garrido de Paz と共著で解説書『Hexagonal Architecture Explained』を2024年に出し、2025年の改訂版(Updated 1st Edition)では「Ports & Adapters only specifies two layers: inside and outside.」と、規定するのは内側と外側の2つだけだと明言しています。
名前の由来と六角形の意味
ヘキサゴナル(hexagonal)は英語で「六角形の」という意味の形容詞です。ただし原典は、六角形である理由を次のように書いています。
The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as they need, not being constrained by a one-dimensional layered drawing.
六角形は、上から下へ積む一次元の層の図から離れ、必要なだけポートとアダプターを描き込む余白を取るための形にすぎません。原典はこの図の見た目から hexagonal architecture という呼び名が生まれたと説明しています。「6つの層がある」「6方向に接続する」と読むのは誤りです。
ポートとアダプターの構成要素
中心にあるのはアプリケーション(ビジネスロジック)で、外側の技術とは直接つながりません。境界に置いたポートが、アプリケーションの側から見た「やり取りの約束」(インターフェース)です。アダプターは、HTTP や SQL のような特定技術とポートの間で変換を受け持ちます。
| 区分 | 原典の呼び方 | 向き | アダプターの例 |
|---|---|---|---|
| 駆動側 | primary / driving | 外 → アプリ | Web API、CLI、自動テスト |
| 被駆動側 | secondary / driven | アプリ → 外 | DB、メール送信、外部 API |
原典では primary / secondary と呼び、driving / driven と呼んでもよいと書いています。この区別はユースケースの主アクター(アプリケーションを動かす側)と副アクター(アプリケーションから呼ばれる側)に由来します。
駆動側(primary)ポートとアダプター
駆動側ポートは、アプリケーションが外部に提供する機能の入口です。実装上は「注文を受け付ける」のようなユースケース単位のクラスやインターフェースになります。そこへ Web フレームワークのコントローラー、CLI、テストハーネスといったアダプターがつながります。
原典は、同じアプリケーションを FIT のテストハーネスから動かし、次に GUI をつなぎ、結合テストでは自動テストスクリプトから実 DB に対して動かし、最後に人が本番 DB で使う、という順序を例に挙げています。テスト用か本番用かの違いは、駆動側(テストハーネスか GUI か)と被駆動側(モックDBか実DBか)のアダプターの組み合わせだけで、アプリケーション本体に手を入れずに段階を進められます。
被駆動側(secondary)ポートとアダプター
被駆動側ポートは、アプリケーションが外部に求める機能を、アプリケーション側の言葉で宣言したインターフェースです。「注文を保存する」「注文確定を通知する」のように書き、SQL やメールの API 名は出しません。本番では DB アダプター、テストではメモリ上のフェイクなどのテストダブルに差し替えます。
ここで依存の向きが逆転します。アプリケーションはインターフェースだけを知っていて、それを実装する DB アダプターのほうがアプリケーション側の型に依存します。原典も関連パターンとして Robert C. Martin の依存性逆転の原則(DIP)を挙げ、差し替え可能な被駆動側アダプターを作る手段だとしています。DIP と DI の違いは依存性の注入(DI)と依存性逆転の原則(DIP)の違い|コード例で使い分けで詳しく扱っています。
ポートの数の決め方
原典は「How Many Ports?」の節で、ユースケースごとにポートを切ると数百になり、逆に全部を左右2本にまとめる極端もあり、どちらも最適ではないと書いています。例として、気象通知システムは4本(気象データの入力、管理者、通知先の購読者、購読者DB)、コーヒーマシンの制御も4本(利用者、レシピと価格のDB、抽出機、コインボックス)、病院の投薬システムは3本(看護師、処方DB、投薬装置)を挙げ、Cockburn 自身は2〜4本を好むとしています。これは個人の好みとして書かれたもので、規則ではありません。
原典は、ポート数を間違えても特に害はないように見えるとも書いています。本記事では、やり取りの目的が違うか、単独でテストしたいか、変更の単位が別か、の3点でポートを分けるかどうかを判断することを勧めます。どれにも当たらない境界は、最初は同じポートにまとめておけば十分です。
クリーンアーキテクチャ・オニオンアーキテクチャとの違い
3つとも外部の技術に中核を依存させない点は共通で、違いは内側の構造をどこまで決めているかにあります。
| 項目 | ヘキサゴナル | オニオン | クリーン |
|---|---|---|---|
| 提唱者 | Alistair Cockburn | Jeffrey Palermo | Robert C. Martin |
| 初出 | 2005年 | 2008年7月29日 | 2012年8月13日 |
| 中心に置くもの | アプリケーション | ドメインモデル | エンティティ |
| 内側の層 | 規定なし | 複数層(数は可変) | 同心円(図は4層) |
| 主な関心 | 内と外の境界 | DB を中心から外す | 依存性ルール |
ヘキサゴナルは内側を一つの「アプリケーション」として扱い、その中の分け方を決めていません。内側をドメイン層とアプリケーション層に分けたくなったとき、その分け方を与えるのがオニオンやクリーンです。
クリーンアーキテクチャとの違い
Robert C. Martin が2012年8月13日に公開した「The Clean Architecture」は、冒頭で先行するアーキテクチャとして Hexagonal Architecture(a.k.a. Ports and Adapters)、Onion Architecture、Screaming Architecture、DCI、BCE を並べ、それらを1つの考え方に統合する試みだと述べています。中核のルールは「ソースコードの依存は内側にしか向けてはならない」という依存性ルール(The Dependency Rule)です。
ヘキサゴナルとの差は、内側を Entities と Use Cases に分けて役割を決めている点です。駆動側と被駆動側の区別は、クリーンでは Controllers・Presenters・Gateways といった外側の円の要素として描かれます。4層構造と SOLID 原則との関係はクリーンアーキテクチャとは?4層構造・SOLID原則・依存性逆転を実装目線で解説で扱っています。
両者を組み合わせて解説する書籍もあり、Tom Hombergs『手を動かしてわかるクリーンアーキテクチャ ヘキサゴナルアーキテクチャによるクリーンなアプリケーション開発』(インプレス、2024年7月19日発売。原書第2版は2023年7月)がその例です。
オニオンアーキテクチャとの違い
Jeffrey Palermo は2008年7月29日のブログ記事「The Onion Architecture: part 1」で、すべての結合は中心へ向かうという基本ルールを示し、最も内側にドメインモデルを置きました。リポジトリのインターフェースはコアに置き、DB を使う実装は外側に出します。記事中の「The database is not the center. It is external.」という一文が、この設計の立場をよく表しています。
Palermo 自身も同じ記事で、ヘキサゴナルとオニオンはインフラを外に出し、アダプターのコードを書いて密結合を避けるという前提を共有していると書いています。違いは、オニオンがドメインモデルを中心とする同心円の層を規定する点です。層ごとの役割はオニオンアーキテクチャとは?4層構成と依存性逆転をクリーン・レイヤードとの違いから解説を参照してください。
レイヤードアーキテクチャとの違い
一般的なレイヤードアーキテクチャは「プレゼンテーション → ビジネスロジック → データアクセス」と上から下へ依存します。データアクセス層を抽象化しておけば実装の交換はできますが、ビジネスロジックが ORM のモデルや SQL を組み立てるクラスのような具体的な型を直接参照していると、DB の差し替えやメモリ上の実装でのテストにはロジック側の書き換えが必要になります。Palermo がオニオンの記事で最も多い問題として挙げたのも、UI とビジネスロジックがデータアクセスに結合することでした。
ヘキサゴナルでは、データアクセスのインターフェース(被駆動側ポート)をアプリケーション側が持ち、DB アダプターがそれを実装します。UI も DB も「外側」という同じ扱いになり、上下ではなく内外で考えるのが最大の違いです。
Python で書くヘキサゴナルアーキテクチャの最小実装
「注文を受け付けて保存し、通知する」だけの例で、ポート・アダプター・組み立ての3つを示します。以下のコードは Python 3.13.14、pytest 9.1.1、import-linter 2.15 で動かし、テスト2件が通ることを確認しています。再現するときは、shop と tests を置いたディレクトリで仮想環境を作り、python -m pip install pytest==9.1.1 import-linter==2.15 を実行してください。保存と通知を1つのトランザクションにまとめる処理や再試行は、最小例のため扱っていません。
ディレクトリ構成と依存の向き
shop/
domain/order.py # ドメインのルール(外部を知らない)
application/ports.py # 被駆動側ポート(Protocol)
application/place_order.py # 駆動側ポート(ユースケース)
adapters/sqlite_repository.py
adapters/console_notifier.py
main.py # 組み立て(composition root)
tests/test_place_order.py
依存の向きは main → adapters → application → domain の順で、外側から内側への参照だけを許します。adapters が domain を直接参照するような層の飛び越しは許容し、application から adapters、domain から application のように、この順序に逆らう import はすべて禁じます。
typing.Protocol によるポートの定義
Python では、PEP 544 で Python 3.8 に入った typing.Protocol がポートの記述に向いています。Protocol は構造的部分型なので、アダプター側が Protocol を継承しなくても、必要なメソッドを持ち、その引数と戻り値の型が互換であれば、型チェッカーが適合を判定します。アプリケーションがアダプターの存在を知らずに済みます。
# shop/domain/order.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
order_id: str
sku: str
quantity: int
def __post_init__(self) -> None:
if self.quantity <= 0:
raise ValueError("quantity must be positive")
# shop/application/ports.py
from typing import Protocol
from shop.domain.order import Order
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
def find(self, order_id: str) -> Order | None: ...
class Notifier(Protocol):
def order_placed(self, order: Order) -> None: ...
# shop/application/place_order.py
import uuid
from shop.application.ports import Notifier, OrderRepository
from shop.domain.order import Order
class PlaceOrder:
def __init__(self, repo: OrderRepository, notifier: Notifier) -> None:
self._repo = repo
self._notifier = notifier
def __call__(self, sku: str, quantity: int) -> str:
order = Order(order_id=uuid.uuid4().hex, sku=sku, quantity=quantity)
self._repo.save(order)
self._notifier.order_placed(order)
return order.order_id
PlaceOrder が駆動側ポート、OrderRepository と Notifier が被駆動側ポートです。ポートのメソッド名が「保存する」「確定を通知する」というアプリケーションの言葉になっている点に注意してください。execute_sql や send_mail のような技術の言葉が出てきたら、アダプターの関心がポートに漏れています。
アダプターと組み立て
# shop/adapters/sqlite_repository.py
import sqlite3
from shop.domain.order import Order
class SqliteOrderRepository:
def __init__(self, path: str = ":memory:") -> None:
self._conn = sqlite3.connect(path)
self._conn.execute(
"CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, sku TEXT, qty INTEGER)"
)
def save(self, order: Order) -> None:
self._conn.execute(
"INSERT INTO orders VALUES (?, ?, ?)",
(order.order_id, order.sku, order.quantity),
)
self._conn.commit()
def find(self, order_id: str) -> Order | None:
row = self._conn.execute(
"SELECT id, sku, qty FROM orders WHERE id = ?", (order_id,)
).fetchone()
return Order(*row) if row else None
# shop/adapters/console_notifier.py
from shop.domain.order import Order
class ConsoleNotifier:
def order_placed(self, order: Order) -> None:
print(f"placed {order.sku} x{order.quantity}")
# shop/main.py
import sys
from shop.adapters.console_notifier import ConsoleNotifier
from shop.adapters.sqlite_repository import SqliteOrderRepository
from shop.application.place_order import PlaceOrder
def main() -> None:
place_order = PlaceOrder(SqliteOrderRepository("orders.db"), ConsoleNotifier())
sku, qty = sys.argv[1], int(sys.argv[2])
print(place_order(sku, qty))
if __name__ == "__main__":
main()
python -m shop.main SKU-9 3 を実行すると placed SKU-9 x3 と注文IDが出力されます。どのアダプターを差し込むかを知っているのは main.py だけです。DB を PostgreSQL に替えるときも、ポートの契約(save と find の引数と戻り値)を保ったアダプターを1つ書き、main.py の import と生成処理、接続設定を変えれば済みます。application と domain には触れません。
テストでのアダプター差し替え
原典の目的だった「DB が無くてもテストできる」は、被駆動側ポートにメモリ上の実装を差し込むだけで実現できます。
# tests/test_place_order.py
import pytest
from shop.application.place_order import PlaceOrder
from shop.domain.order import Order
class InMemoryOrderRepository:
def __init__(self) -> None:
self.orders: dict[str, Order] = {}
def save(self, order: Order) -> None:
self.orders[order.order_id] = order
def find(self, order_id: str) -> Order | None:
return self.orders.get(order_id)
class SpyNotifier:
def __init__(self) -> None:
self.sent: list[Order] = []
def order_placed(self, order: Order) -> None:
self.sent.append(order)
def test_place_order_saves_and_notifies() -> None:
repo, notifier = InMemoryOrderRepository(), SpyNotifier()
order_id = PlaceOrder(repo, notifier)("SKU-1", 2)
assert repo.find(order_id) == Order(order_id, "SKU-1", 2)
assert notifier.sent == [repo.find(order_id)]
def test_zero_quantity_is_rejected() -> None:
with pytest.raises(ValueError):
PlaceOrder(InMemoryOrderRepository(), SpyNotifier())("SKU-1", 0)
プロジェクトのルートで python -m pytest -q を実行すると 2 passed になります。素の pytest -q では shop が import パスに入らず、収集の段階でエラーになる点に注意してください。InMemoryOrderRepository はフェイク、SpyNotifier は呼び出しを記録するスパイにあたります。使い分けはテストダブル(TestDouble)とは|5分類の違いと使い分けで整理しています。
Go と TypeScript でポートを書くときの違い
Go のインターフェースは宣言なしで満たされる(言語仕様の Implementing an interface)ため、Python の Protocol と同じ感覚でポートを書けます。Go の公式 Code Review Comments は、インターフェースは実装する側ではなく、そのインターフェース型の値を使う側のパッケージに置くのが一般的だとしています。これはポートをアプリケーション側に置くヘキサゴナルの考え方とそのまま一致します。
// アプリケーション側のパッケージでポートを宣言する
type OrderRepository interface {
Save(id, sku string, qty int) error
}
// アダプター:implements の宣言なしで OrderRepository を満たす
type MemoryRepo struct{ rows map[string]int }
func (m *MemoryRepo) Save(id, sku string, qty int) error {
if m.rows == nil {
m.rows = map[string]int{}
}
m.rows[id] = qty
return nil
}
var _ OrderRepository = (*MemoryRepo)(nil) // 満たしていなければコンパイルエラー
上は宣言部分の抜粋で、単独で動かすには package main と import "fmt"、呼び出し側の main 関数を補ってください(補った全体を go1.26.5 で実行して確認済み)。最後の行は、アダプターがポートを満たしているかをコンパイル時に確かめるための慣用句です。TypeScript も構造的型付けなので implements は任意ですが、書いておくとポート変更時にアダプター側がコンパイルエラーになり、修正漏れに気づけます。
依存方向を機械的に守らせる方法
ヘキサゴナルアーキテクチャは、ディレクトリを分けただけでは維持できません。締め切り前に application からアダプターや DB ライブラリを直接 import する1行が入ると、その時点で内外の境界は崩れます。レビューで毎回見つけるのは難しいので、依存検査を CI に入れておくのが確実です。
Python では import-linter(2.15、Python 3.10 以上)の layers 契約が使えます。layers 契約は、上位の層は下位の層に依存してよいが、その逆は許さないという規則を検査します。
# .importlinter
[importlinter]
root_package = shop
[importlinter:contract:hexagon]
name = Dependencies point inward
type = layers
layers =
shop.main
shop.adapters
shop.application
shop.domain
この設定で lint-imports を実行すると、上の実装例では契約が守られ(kept)、終了コード0で終わります。試しに place_order.py へ from shop.adapters.sqlite_repository import SqliteOrderRepository を1行足すと、次のように違反箇所の行番号まで出て、終了コード1で止まります。
Broken contracts
----------------
Dependencies point inward
-------------------------
shop.application is not allowed to import shop.adapters:
- shop.application.place_order -> shop.adapters.sqlite_repository (l.2)
ただし layers 契約が見るのは shop 内の層どうしの依存だけで、application に import sqlite3 を足しても Contracts: 1 kept, 0 broken. のまま通ります。DB ドライバーなど外部パッケージへの直接依存も止めたい場合は、forbidden 契約を追加します。
# .importlinter に追記([importlinter] 節に include_external_packages = True も必要)
[importlinter:contract:no-db-in-core]
name = Core must not import DB drivers
type = forbidden
source_modules =
shop.application
shop.domain
forbidden_modules =
sqlite3
この状態で import sqlite3 を足すと shop.application is not allowed to import sqlite3 と出て終了コード1になります。どちらの契約も違反時に終了コード1を返すので、CI のジョブにそのまま組み込めます。Java では ArchUnit のライブラリ API に onionArchitecture() があり、ユーザーガイドは Onion Architecture を「Hexagonal Architecture」「Ports and Adapters」の別名として扱ったうえでこの API を説明しています。domainModels・applicationServices・adapter をパッケージ単位で指定し、ドメインからアダプターへの参照や、別々に定義したアダプター同士の参照を検出します。モジュール境界を同様に検証する考え方はモジュラモノリスとは?モジュール境界の設計とDB分割・依存検証の実装でも扱っています。
ヘキサゴナルアーキテクチャを採用すべき場面と避けるべき場面
ヘキサゴナルが効く条件:入口の共用と外部依存の分離
効果がはっきり出るのは、外部との接点が複数あり、しかも入れ替わる見込みがある場合です。Web API とバッチの両方から同じ処理を呼ぶ、決済や配送の外部サービスを将来替える可能性がある、外部サービスのサンドボックスが不安定でテストが止まりがち、といった状況です。上の例の InMemoryOrderRepository や SpyNotifier のように被駆動側ポートへテストダブルを差し込めば、外部サービスが落ちていてもユースケースのテストを回せます。UI や接続先が未確定のうちから本体を作り始めたい場合も向いています。
ヘキサゴナルを採用しない判断と導入後の境界崩壊
ビジネスルールがほとんど無く、画面の入力を DB に保存して一覧を返すだけの CRUD には勧めません。ポート・アダプター・組み立てのコードがロジック本体より多くなり、項目の追加のようにポートの契約を変える変更では、ポートとアダプターとテストダブルをまとめて直すことになります。数週間で捨てるプロトタイプも同じです。外部依存が DB 1つだけで替える予定も無いなら、実 DB を使う結合テストで十分なことが多いでしょう。
導入後によく起きる失敗は次の3つです。
- ORM のモデルクラスをそのままドメインの型として使い、ポートの引数や戻り値に ORM 固有の型が出てくる。
- 差し替える予定も境界を引く理由も無いまま、テーブルごと・エンドポイントごとにポートを作り、役割の重なるインターフェースが増えていく。
- アダプター同士が直接呼び合い、アプリケーションを経由しない処理の流れができる。
3つ目のアダプター同士の直接参照は、ArchUnit の onionArchitecture() や import-linter の independence 契約のような依存検査で機械的に止められます。一方、1つ目と2つ目はポートの設計判断の問題なので、ポートの引数に技術固有の型が出ていないか、そのポートを差し替える予定があるかをレビュー観点に入れてください。機能ごとに縦に切るほうが合うチームなら、Vertical Slice Architectureとは?機能単位で縦割りする設計と実装例も比較対象になります。
DDD(ドメイン駆動設計)との関係
ヘキサゴナルアーキテクチャは DDD の一部ではなく、DDD を前提ともしていません。原典が問題にしているのは内側と外側の分離だけで、内側をどうモデリングするかには踏み込んでいないからです。
ただし組み合わせやすく、DDD のリポジトリは被駆動側ポートに、ユースケースを実行するアプリケーションサービスは駆動側ポートに対応します。DDD の用語と採用判断はドメイン駆動設計(DDD)とは?意味・基本用語・実装と採用判断をわかりやすく解説、既存コードへの段階的な導入はDDDの実装手順:値オブジェクトから集約まで既存コードを段階移行するTypeScript実例で解説しています。
ヘキサゴナルアーキテクチャに関するよくある質問
ヘキサゴナルとはどういう意味ですか?
英語の hexagonal で「六角形の」という意味です。辺の数が6であることに意味は無く、上下の層の図から離れて内と外の境界を描き、ポートとアダプターを描き込む余白を取るための形です。正式なパターン名は Ports and Adapters です。
ヘキサゴンアーキテクチャとヘキサゴナルアーキテクチャは違うものですか?
同じものです。名詞 hexagon と形容詞 hexagonal の違いによる表記ゆれで、ポートアンドアダプターも同じ設計を指します。
クリーンアーキテクチャとの違いは何ですか?
依存を内側へ向ける点は同じです。ヘキサゴナルは内と外の境界だけを定め、2012年に Robert C. Martin が示したクリーンアーキテクチャは内側を Entities と Use Cases に分けて役割まで決めています。
オニオンアーキテクチャとはどう違いますか?
オニオンは2008年に Jeffrey Palermo が示した設計で、ドメインモデルを中心にした同心円の層を規定します。インフラを外に出す前提はヘキサゴナルと共通です。
Python でも使えますか?
使えます。typing.Protocol(Python 3.8 以降)でポートを定義すると、アダプター側に継承を書かずに済みます。依存方向は import-linter の layers 契約で CI から検査できます。