ひとことで言うと#
アプリケーションの中心に**ビジネスロジック(ドメイン)を置き、外部との接点をポート(インターフェース)とアダプター(実装)**で抽象化することで、DB・UI・外部APIなどの詳細を簡単に差し替えられるアーキテクチャ。別名「ポート&アダプター」。
押さえておきたい用語#
- ポート(Port)
- ドメインが外部とやり取りするためのインターフェース(契約)。「何をするか」だけ定義し、実装の詳細は含まない。
- アダプター(Adapter)
- ポートの具体的な実装。REST Controller、PostgresRepository、InMemoryRepositoryなど、技術的な詳細を担う。
- 駆動側ポート(Primary Port)
- 外部からドメインを呼び出すためのインターフェース。ユースケースの入り口にあたる。
- 被駆動側ポート(Secondary Port)
- ドメインが外部リソースを利用するためのインターフェース。DBやメール送信など出力側にあたる。
- 依存性注入(Dependency Injection)
- アプリケーション起動時にポートに対してアダプターの実装を外部から渡す設計パターン。テスト時のモック差し替えを可能にする。
ヘキサゴナルアーキテクチャの全体像#
こんな悩みに効く#
- ビジネスロジックがフレームワークやDBに依存していて、テストが書きにくい
- 外部サービスを別のものに差し替えたいが、コード全体に影響が出る
- アプリケーションの構造が明確でなく、どこに何を書くべきか迷う
基本の使い方#
外部に一切依存しない純粋なビジネスロジックを中心に配置する。
- ドメインオブジェクト、ビジネスルール、ユースケースをここに書く
- フレームワーク、DB、HTTPなどの技術的な関心事を排除する
- プレーンなクラスやモジュールで構成する
ポイント: ここだけ見ればビジネスの振る舞いがわかる状態を目指す。
ドメインが外部とやり取りするための**契約(インターフェース)**を定義する。
- 駆動側ポート(Primary): 外部からドメインを呼び出すためのインターフェース(例: OrderService)
- 被駆動側ポート(Secondary): ドメインが外部リソースを利用するためのインターフェース(例: OrderRepository)
- ポートはドメイン側に属する
ポイント: ポートは「何をするか」だけ定義し、「どうやるか」は書かない。
ポートの具体的な実装を外側に作る。
- 駆動側アダプター: REST Controller、CLI、gRPCハンドラー等
- 被駆動側アダプター: PostgresOrderRepository、StripePaymentGateway等
- アダプターはいつでも差し替え可能
ポイント: テスト時はモックアダプターに差し替えるだけで、ドメインを単体テストできる。
アプリケーションの起動時に、どのアダプターをどのポートに接続するかを設定する。
- DIコンテナやファクトリーで組み立てる
- 環境によってアダプターを切り替える(本番: PostgreSQL、テスト: InMemory)
- 設定は1箇所に集約する
ポイント: 組み立て(Configuration)はアプリのエントリーポイントで行う。ドメインは自分が何に繋がれるか知らない。
具体例#
ドメイン: NotificationService がビジネスルールを持つ。「VIPユーザーには即座に通知、一般ユーザーには1日1回のバッチ通知」のルール。
ポート(駆動側): SendNotificationUseCase インターフェース。外部から通知送信を依頼するための窓口。
ポート(被駆動側): NotificationSender インターフェース、UserRepository インターフェース。
アダプター(駆動側): NotificationController(REST API)、NotificationEventHandler(Kafkaコンシューマー)。
アダプター(被駆動側): EmailNotificationSender、SlackNotificationSender、PostgresUserRepository。
テスト時: InMemoryNotificationSender と InMemoryUserRepository に差し替え。DB不要でテスト実行時間が12秒から0.3秒に短縮。
後日「LINE通知も追加したい」という要件が来た時、LineNotificationSender アダプターを追加するだけで対応完了。ドメインのコードは一切変更なし。
状況: ECサイトの決済をStripeで処理していたが、手数料削減のためにGMOペイメントへの切り替えを検討。しかし決済ロジックがStripe SDKに密結合しており、移行見積りは3人月。
ヘキサゴナル適用:
- ポート:
PaymentGatewayインターフェースを定義(charge()、refund()、getStatus()) - アダプター:
StripePaymentGateway(既存)とGmoPaymentGateway(新規)を実装 - DI: 環境変数
PAYMENT_PROVIDERで切り替え可能に
結果: GMOアダプターの実装は1人で2週間で完了。ドメインの決済ロジック(金額計算、割引適用、税計算)は一切変更なし。移行見積りが3人月から0.5人月に削減。
状況: MySQL 5.7からPostgreSQL 16への移行が必要。全テーブル42本、移行中もサービスは止められない。
ヘキサゴナル設計の活用:
- 被駆動側ポート
OrderRepositoryに対して、MySqlOrderRepositoryとPostgresOrderRepositoryの2つのアダプターを用意 DualWriteOrderRepositoryを作成:書き込みは両方に行い、読み取りはMySQLから- 整合性確認後、読み取りもPostgreSQLに切り替え
- 最後にMySQLアダプターを削除
結果: ダウンタイムゼロで42テーブルの移行を3週間で完了。問題発生時はDIの設定変更だけでMySQL読み取りに即座にフォールバック可能だったため、チームの心理的安全性も高かった。
やりがちな失敗パターン#
- ドメインに技術的な関心事が漏れ込む — ドメイン層でHTTPステータスコードやSQL文を扱ってしまうと、アーキテクチャの意味がなくなる。ドメインは純粋なビジネスロジックだけにすること
- ポートを作りすぎる — すべてのメソッドに個別のインターフェースを定義すると、ファイル数が爆発する。関連する操作はまとめて1つのポートにすること
- 小さなプロジェクトにフル適用する — CRUD中心の小規模アプリには過剰設計になる。ビジネスロジックが複雑で、外部依存の差し替えが求められる場合に適用する
- テストでのみモックを使い、本番ではDIを使わない — 本番コードでnewしてしまうと、差し替え可能性が失われる。本番でもDIコンテナを通じてアダプターを注入することを徹底する
まとめ#
ヘキサゴナルアーキテクチャは 「ビジネスロジックを外部依存から守る」 ための設計手法。ポートで契約を定義し、アダプターで具体的な技術を差し込む。テスタビリティと柔軟性が大幅に向上するが、小規模なプロジェクトでは過剰設計になりうる。適用すべきかどうかは、プロジェクトの複雑さに合わせて判断しよう。