【アーキテクチャ】ヘキサゴナルアーキテクチャ(ポート&アダプター) - j-komatsu/myCheatSheet GitHub Wiki
ヘキサゴナルアーキテクチャ(Hexagonal Architecture) は、ビジネスロジックを外部依存から完全に分離し、テスタビリティと保守性を高めるアーキテクチャパターンです。別名「ポート&アダプターパターン」とも呼ばれます。
graph TD
subgraph "外部世界(Outside)"
UI[Web UI]
REST[REST API]
CLI[CLI]
DB[(Database)]
EXT[External API]
MQ[Message Queue]
end
subgraph "ヘキサゴン(Hexagon)"
CORE[ビジネスロジック<br/>Domain Model]
end
subgraph "アダプター層"
WA[Web Adapter]
RA[REST Adapter]
CA[CLI Adapter]
DBA[DB Adapter]
EXTA[API Adapter]
MQA[MQ Adapter]
end
UI --> WA
REST --> RA
CLI --> CA
WA --> CORE
RA --> CORE
CA --> CORE
CORE --> DBA
CORE --> EXTA
CORE --> MQA
DBA --> DB
EXTA --> EXT
MQA --> MQ
style CORE fill:#f9f,stroke:#333,stroke-width:4px
| 概念 | 説明 |
|---|---|
| ヘキサゴン(六角形) | ビジネスロジックのコア。外部依存なし |
| ポート(Port) | インターフェース。内部と外部の契約 |
| アダプター(Adapter) | ポートの具体的な実装。外部技術と接続 |
| プライマリポート | アプリケーションを駆動する入口(API等) |
| セカンダリポート | アプリケーションが利用する出口(DB等) |
| メリット | 詳細 |
|---|---|
| テスタビリティ | 外部依存をモック化しやすい |
| 技術の差し替え可能性 | DBやフレームワークを容易に変更可能 |
| ビジネスロジックの独立性 | フレームワークに依存しない純粋なドメインモデル |
| 複数のUI対応 | Web、CLI、APIなど複数インターフェース共存可能 |
| 保守性の向上 | 関心の分離が明確 |
| 課題 | 対策 |
|---|---|
| 初期コスト | 小規模プロジェクトでは過剰設計に注意 |
| インターフェース数の増加 | 適切な粒度で設計 |
| 学習コスト | チーム全体での理解が必要 |
| ボイラープレートコード | IDEのコード生成機能を活用 |
src/
├── domain/ # ヘキサゴンのコア
│ ├── model/ # ドメインモデル
│ │ ├── Order.java
│ │ └── OrderItem.java
│ ├── service/ # ドメインサービス
│ │ └── OrderService.java
│ └── port/ # ポート(インターフェース)
│ ├── in/ # プライマリポート(入力)
│ │ ├── CreateOrderUseCase.java
│ │ └── GetOrderUseCase.java
│ └── out/ # セカンダリポート(出力)
│ ├── OrderRepository.java
│ ├── PaymentGateway.java
│ └── NotificationService.java
│
├── adapter/ # アダプター層
│ ├── in/ # 入力アダプター
│ │ ├── web/
│ │ │ └── OrderController.java
│ │ ├── cli/
│ │ │ └── OrderCli.java
│ │ └── messaging/
│ │ └── OrderEventListener.java
│ └── out/ # 出力アダプター
│ ├── persistence/
│ │ ├── JpaOrderRepository.java
│ │ └── OrderEntity.java
│ ├── payment/
│ │ └── StripePaymentAdapter.java
│ └── notification/
│ └── EmailNotificationAdapter.java
│
└── config/ # 依存性注入設定
└── ApplicationConfig.java
// domain/model/Order.java
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private OrderStatus status;
private final Money totalAmount;
public Order(CustomerId customerId, List<OrderItem> items) {
this.id = OrderId.generate();
this.customerId = customerId;
this.items = List.copyOf(items);
this.status = OrderStatus.PENDING;
this.totalAmount = calculateTotal(items);
}
// ビジネスロジック
public void confirm() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Order can only be confirmed when pending");
}
this.status = OrderStatus.CONFIRMED;
}
public void cancel(String reason) {
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Cannot cancel shipped order");
}
this.status = OrderStatus.CANCELLED;
}
private Money calculateTotal(List<OrderItem> items) {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// getters...
}// domain/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderId createOrder(CreateOrderCommand command);
}
// domain/port/in/CreateOrderCommand.java
public record CreateOrderCommand(
CustomerId customerId,
List<OrderItemDto> items,
ShippingAddress shippingAddress
) {}// domain/port/out/OrderRepository.java
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId orderId);
List<Order> findByCustomerId(CustomerId customerId);
}
// domain/port/out/PaymentGateway.java
public interface PaymentGateway {
PaymentResult processPayment(OrderId orderId, Money amount);
}
// domain/port/out/NotificationService.java
public interface NotificationService {
void sendOrderConfirmation(Order order);
}// domain/service/OrderService.java
@Service
@Transactional
public class OrderService implements CreateOrderUseCase, ConfirmOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final NotificationService notificationService;
// コンストラクタインジェクション(ポートに依存)
public OrderService(
OrderRepository orderRepository,
PaymentGateway paymentGateway,
NotificationService notificationService
) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.notificationService = notificationService;
}
@Override
public OrderId createOrder(CreateOrderCommand command) {
// ドメインモデルを生成
List<OrderItem> items = command.items().stream()
.map(dto -> new OrderItem(dto.productId(), dto.quantity(), dto.price()))
.toList();
Order order = new Order(command.customerId(), items);
// 永続化(セカンダリポート経由)
orderRepository.save(order);
// 決済処理
PaymentResult result = paymentGateway.processPayment(
order.getId(),
order.getTotalAmount()
);
if (result.isSuccess()) {
order.confirm();
orderRepository.save(order);
notificationService.sendOrderConfirmation(order);
}
return order.getId();
}
}// adapter/in/web/OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
private final GetOrderUseCase getOrderUseCase;
public OrderController(
CreateOrderUseCase createOrderUseCase,
GetOrderUseCase getOrderUseCase
) {
this.createOrderUseCase = createOrderUseCase;
this.getOrderUseCase = getOrderUseCase;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Valid CreateOrderRequest request
) {
// リクエストDTOをコマンドに変換
CreateOrderCommand command = new CreateOrderCommand(
new CustomerId(request.customerId()),
request.items(),
request.shippingAddress()
);
OrderId orderId = createOrderUseCase.createOrder(command);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new OrderResponse(orderId.getValue()));
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderDetailResponse> getOrder(@PathVariable String orderId) {
OrderView order = getOrderUseCase.getOrder(new OrderId(orderId));
return ResponseEntity.ok(OrderDetailResponse.from(order));
}
}// adapter/out/persistence/JpaOrderRepository.java
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository springDataRepo;
private final OrderMapper mapper;
@Override
public void save(Order order) {
OrderEntity entity = mapper.toEntity(order);
springDataRepo.save(entity);
}
@Override
public Optional<Order> findById(OrderId orderId) {
return springDataRepo.findById(orderId.getValue())
.map(mapper::toDomain);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return springDataRepo.findByCustomerId(customerId.getValue()).stream()
.map(mapper::toDomain)
.toList();
}
}
// JPA Entity(永続化の詳細)
@Entity
@Table(name = "orders")
class OrderEntity {
@Id
private String id;
private String customerId;
private String status;
private BigDecimal totalAmount;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItemEntity> items;
// getters/setters...
}// adapter/out/payment/StripePaymentAdapter.java
@Component
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult processPayment(OrderId orderId, Money amount) {
try {
// Stripe APIを呼び出し
PaymentIntent intent = stripeClient.createPaymentIntent(
amount.getAmount(),
amount.getCurrency()
);
return new PaymentResult(
true,
intent.getId(),
"Payment processed successfully"
);
} catch (StripeException e) {
return new PaymentResult(
false,
null,
"Payment failed: " + e.getMessage()
);
}
}
}graph LR
subgraph "外部(Infrastructure)"
WEB[Web Controller]
JPA[JPA Repository]
end
subgraph "ポート(Interface)"
IN[CreateOrderUseCase]
OUT[OrderRepository]
end
subgraph "ドメイン層"
SERVICE[OrderService]
end
WEB -->|depends on| IN
SERVICE -.->|implements| IN
SERVICE -->|depends on| OUT
JPA -.->|implements| OUT
style SERVICE fill:#f9f,stroke:#333,stroke-width:4px
ポイント:
- ✅ 外部層(Adapter)はドメイン層(Port)に依存
- ✅ ドメイン層は外部に依存しない
- ✅ すべての依存が「内向き」
| アーキテクチャ | ヘキサゴナルとの関係 |
|---|---|
| クリーンアーキテクチャ | 同じ思想。クリーンは円形、ヘキサゴナルは六角形で表現 |
| レイヤードアーキテクチャ | ヘキサゴナルのほうが依存性逆転を明確化 |
| オニオンアーキテクチャ | ヘキサゴナルとほぼ同義 |
| モジュラモノリス | 各モジュール内部でヘキサゴナルを適用可能 |
| DDD | ドメインモデルの設計思想を共有 |
❌ 問題
- インターフェースが多すぎて管理が大変
✅ 解決策
// 粒度を適切に保つ
// ❌ 細かすぎる
interface SaveOrder {}
interface FindOrder {}
interface UpdateOrder {}
// ✅ 適切な粒度
interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
void delete(OrderId id);
}❌ 問題
- マッピングコードが冗長
✅ 解決策
// MapStructを活用
@Mapper(componentModel = "spring")
interface OrderMapper {
OrderEntity toEntity(Order domain);
Order toDomain(OrderEntity entity);
}✅ 解決策
// ユースケース(ドメインサービス)にトランザクションを配置
@Service
@Transactional // ← ここでトランザクション管理
public class OrderService implements CreateOrderUseCase {
// ...
}
// アダプター層ではトランザクション不要
@Repository
public class JpaOrderRepository implements OrderRepository {
// トランザクションなし
}// application.yml で切り替え
@Configuration
public class RepositoryConfig {
@Bean
@ConditionalOnProperty(name = "storage.type", havingValue = "jpa")
public OrderRepository jpaOrderRepository(SpringDataOrderRepository repo) {
return new JpaOrderRepository(repo);
}
@Bean
@ConditionalOnProperty(name = "storage.type", havingValue = "mongodb")
public OrderRepository mongoOrderRepository(MongoTemplate template) {
return new MongoOrderRepository(template);
}
}-
ドメイン層は外部に依存しない
- Spring、JPA、外部ライブラリに依存しない純粋なJava
-
ポートはドメイン層に配置
- インターフェースはビジネスロジック側が定義
-
アダプターは差し替え可能
- テスト時はモック、本番はJPA等
-
ユースケースを明示
-
CreateOrderUseCaseのように意図が明確
-
✅ 推奨
- ビジネスロジックが複雑
- 長期運用が予想されるシステム
- 外部依存(DB、API等)が頻繁に変わる可能性
- テスタビリティ重視
❌ 過剰設計の可能性
- シンプルなCRUDアプリ
- プロトタイプ・PoC
- 小規模な内部ツール
- Port(ポート): ドメイン層が外部とやり取りするインターフェース
- Adapter(アダプター): ポートの具体的な実装(技術的詳細)
- Primary Port: アプリケーションを駆動する入力(API、CLI等)
- Secondary Port: アプリケーションが利用する出力(DB、外部API等)
- Driving Side: アプリケーションを使う側(UI、API呼び出し側)
- Driven Side: アプリケーションが使う側(DB、外部サービス)
- 📖 Alistair Cockburn - Hexagonal Architecture
- 📖 Tom Hombergs著『Get Your Hands Dirty on Clean Architecture』
- 🌐 Netflix - Hexagonal Architecture with Spring Boot
- 📖 Vaughn Vernon著『実践ドメイン駆動設計』
課題1: シンプルなTODOアプリをヘキサゴナルアーキテクチャで実装してみましょう
- プライマリポート:
CreateTodoUseCase,GetTodoListUseCase - セカンダリポート:
TodoRepository - 入力アダプター: REST API
- 出力アダプター: InMemory実装 → JPA実装へ差し替え
課題2: 既存のレイヤードアーキテクチャをヘキサゴナルにリファクタリングしてみましょう
課題3: 同じポートに対して複数のアダプター(JPA/MongoDB/InMemory)を実装し、設定で切り替えてみましょう
- 【アーキテクチャ】クリーンアーキテクチャ - ヘキサゴナルの発展形
- 【アーキテクチャ】ドメイン駆動設計(DDD) - ドメインモデルの設計手法
- 【アーキテクチャ】モジュラモノリス - モジュール内部でヘキサゴナルを適用
- 【アーキテクチャ】CQRS(コマンドクエリ責任分離) - ヘキサゴナルと組み合わせ可能
- 【プログラミング】【Java】Spring Framework - DIコンテナの活用
📝 最終更新: 2025-10-25