mcpField
H
hexagonal-architecture

hexagonal-architecture

github.com/rrezartprebreza開発345109 回インストール2 日前に更新
リポジトリ

hexagonal-architecture について

Spring を使う Java プロジェクト向けのヘキサゴナルアーキテクチャのスキルです。フレームワークに依存しないドメイン層、ポート、アプリケーション層、インフラのアダプターによる構成を案内します。

主な機能

  1. 01ドメイン、アプリケーション、インフラ層
  2. 02フレームワークに依存しないドメインモデル
  3. 03駆動ポートと被駆動ポートの分離
  4. 04ドメインポートを実装するアダプター
  5. 05よくあるアーキテクチャ上の落とし穴

活用シーン

  1. 01Java プロジェクトのパッケージ構成の設計
  2. 02永続化実装とユースケースの分離
  3. 03ドメインロジックや Spring アノテーションの配置確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill hexagonal-architecture

インストール後、Claude Code で/hexagonal-architectureを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
examples/
bad-use-case.java
good-use-case.java
templates/
OrderJpaAdapter.java
OrderJpaEntity.java

詳細

ヘキサゴナルアーキテクチャ

パッケージ構成

src/main/java/com/example/
├── domain/                     ← Pure Java. Zero framework dependencies.
│   ├── model/                  ← Entities, value objects, aggregates
│   ├── port/
│   │   ├── in/                 ← Use case interfaces (driving ports)
│   │   └── out/                ← Repository/external interfaces (driven ports)
│   └── service/                ← Domain services (pure business logic)
├── application/                ← Orchestrates use cases. Spring allowed here.
│   └── usecase/                ← @Service implementations of domain ports
└── infrastructure/             ← All framework/DB/HTTP details
    ├── persistence/            ← JPA adapters implementing out ports
    ├── web/                    ← REST controllers (driving adapters)
    └── external/               ← HTTP clients, messaging adapters

ドメイン層 — Spring 依存なし

// domain/model/Order.java — pure Java, no annotations
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private OrderStatus status;
    private final List<OrderItem> items;

    private Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.status = OrderStatus.PENDING;
        this.items = new ArrayList<>();
    }

    public static Order create(CustomerId customerId) {
        return new Order(OrderId.generate(), customerId);
    }

    public void addItem(ProductId productId, int quantity, Money price) {
        if (status != OrderStatus.PENDING)
            throw new OrderNotModifiableException(id);
        items.add(new OrderItem(productId, quantity, price));
    }

    // Getters only — no setters
}

// domain/model/OrderId.java — value object
public record OrderId(UUID value) {
    public static OrderId generate() { return new OrderId(UUID.randomUUID()); }
    public static OrderId of(String value) { return new OrderId(UUID.fromString(value)); }
}

ポート — インターフェースのみ

// domain/port/in/CreateOrderUseCase.java — driving port
public interface CreateOrderUseCase {
    Order createOrder(CreateOrderCommand command);
}

// domain/port/in/CreateOrderCommand.java
public record CreateOrderCommand(CustomerId customerId, List<OrderItemData> items) {}

// domain/port/out/OrderRepository.java — driven port
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
    void deleteById(OrderId id);
}

// domain/port/out/InventoryPort.java — driven port
public interface InventoryPort {
    void reserve(List<OrderItem> items);
    void release(List<OrderItem> items);
}

アプリケーション層 — ユースケースの実装

// application/usecase/CreateOrderService.java
@Service  // Spring allowed here
@RequiredArgsConstructor
@Transactional
public class CreateOrderService implements CreateOrderUseCase {

    private final OrderRepository orderRepository;   // domain port (not JPA repo)
    private final InventoryPort inventoryPort;        // domain port

    @Override
    public Order createOrder(CreateOrderCommand command) {
        Order order = Order.create(command.customerId());
        command.items().forEach(item ->
            order.addItem(item.productId(), item.quantity(), item.price()));
        inventoryPort.reserve(order.getItems());
        return orderRepository.save(order);
    }
}

インフラストラクチャ — アダプター

// infrastructure/persistence/JpaOrderRepository.java — implements domain port
@Repository
@RequiredArgsConstructor
public class JpaOrderRepository implements OrderRepository {

    private final SpringDataOrderRepository springDataRepo;
    private final OrderMapper mapper;

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = mapper.toEntity(order);
        return mapper.toDomain(springDataRepo.save(entity));
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return springDataRepo.findById(id.value()).map(mapper::toDomain);
    }

    @Override
    public void deleteById(OrderId id) {
        springDataRepo.deleteById(id.value());
    }
}

// Separate Spring Data interface — infrastructure detail
interface SpringDataOrderRepository extends JpaRepository<OrderJpaEntity, UUID> {}

// infrastructure/web/OrderController.java — driving adapter
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {

    private final CreateOrderUseCase createOrderUseCase; // injects use case port

    @PostMapping
    public ResponseEntity<ApiResponse<OrderResponse>> create(@Valid @RequestBody CreateOrderRequest request) {
        Order order = createOrderUseCase.createOrder(request.toCommand());
        return ResponseEntity.status(201).body(ApiResponse.ok(OrderResponse.from(order)));
    }
}

注意点

  • エージェントがドメインクラスで javax.persistence をインポートする — ドメイン層はフレームワークに依存しないようにします
  • エージェントがユースケースに JpaRepository を直接注入する — ドメインのポートインターフェースを使用します
  • エージェントがドメインサービスに @Transactional を付ける — トランザクション管理はアプリケーション層で行います
  • エージェントが駆動ポートと被駆動ポートを混在させる — port/in = アプリケーションが提供するもの、port/out = アプリケーションが必要とするもの
  • エージェントがゲッターとセッターだけの貧血ドメインを作る — 振る舞いはドメインオブジェクトに持たせます

コメント