clean-architecture
Java 21以降・Spring Boot 3.5以降のアプリケーションにおいて、Clean Architecture・ヘキサゴナルアーキテクチャ(Ports & Adapters)・ドメイン駆動設計の実装パターンを提供します。レイヤードアーキテクチャの構成、ドメインロジックとフレームワークの分離、ポートとアダプターの実装、エンティティ/値オブジェクト/集約の作成、またはモノリシックなコードベースのテスタビリティ・保守性向上を目的としたリファクタリングを行う際に活用してください。
description の原文を見る
Provides implementation patterns for Clean Architecture, Hexagonal Architecture (Ports & Adapters), and Domain-Driven Design in Java 21+ Spring Boot 3.5+ applications. Use when structuring layered architectures, separating domain logic from frameworks, implementing ports and adapters, creating entities/value objects/aggregates, or refactoring monolithic codebases for testability and maintainability.
SKILL.md 本文
Clean Architecture、Hexagonal Architecture & DDD for Spring Boot
概要
このスキルは、Java 21+ Spring Boot 3.5+ アプリケーションにおいて Clean Architecture、Hexagonal Architecture(Ports & Adapters)、および Domain-Driven Design のタクティカルパターンを実装するための包括的なガイダンスを提供します。適切なレイヤリングと依存性管理を通じて、関心事の明確な分離、フレームワークに依存しないドメインロジック、および高度にテスト可能なコードベースを実現します。
使用場合
- 関心事の明確な分離を備えた新しい Spring Boot アプリケーションの設計
- 密結合されたコードのテスト可能でレイヤー化されたアーキテクチャへのリファクタリング
- フレームワークとインフラストラクチャから独立したドメインロジックの実装
- スワップ可能な実装のためのポートとアダプターの設計
- Domain-Driven Design のタクティカルパターン(エンティティ、バリューオブジェクト、アグリゲート)の適用
- Spring コンテキスト依存性なしでテスト可能なビジネスロジックの作成
手順
1. コアコンセプトを理解する
Clean Architecture レイヤー(依存性ルール)
依存性は内向きに流れます。内側のレイヤーは外側のレイヤーについて何も知りません。
| レイヤー | 責務 | Spring Boot 相当 |
|---|---|---|
| Domain | エンティティ、バリューオブジェクト、ドメインイベント、リポジトリインターフェース | domain/ - Spring アノテーションなし |
| Application | ユースケース、アプリケーションサービス、DTO、ポート | application/ - @Service、@Transactional |
| Infrastructure | フレームワーク、データベース、外部 API | infrastructure/ - @Repository、@Entity |
| Adapter | コントローラー、プレゼンター、外部ゲートウェイ | adapter/ - @RestController |
Hexagonal Architecture(Ports & Adapters)
- Domain Core: Pure Java のビジネスロジック、フレームワーク依存性なし
- Ports: コントラクトを定義するインターフェース(driven と driving)
- Adapters: 具体的な実装(JPA、REST、メッセージング)
Domain-Driven Design タクティカルパターン
- Entities: アイデンティティとライフサイクルを持つオブジェクト(例:
Order、Customer) - Value Objects: 不変で、属性で定義される(例:
Money、Email) - Aggregates: ルートエンティティを持つ一貫性の境界
- Domain Events: 重要なビジネス上の発生を捕捉
- Repositories: 永続化の抽象化、インフラストラクチャレイヤーで実装
2. パッケージ構造を整理する
以下の機能ベースのパッケージ編成に従ってください:
com.example.order/
├── domain/
│ ├── model/ # エンティティ、バリューオブジェクト
│ ├── event/ # ドメインイベント
│ ├── repository/ # リポジトリインターフェース(ポート)
│ └── exception/ # ドメイン例外
├── application/
│ ├── port/in/ # Driving ポート(ユースケースインターフェース)
│ ├── port/out/ # Driven ポート(外部サービスインターフェース)
│ ├── service/ # アプリケーションサービス
│ └── dto/ # リクエスト/レスポンス DTO
├── infrastructure/
│ ├── persistence/ # JPA エンティティ、リポジトリアダプター
│ └── external/ # 外部サービスアダプター
└── adapter/
└── rest/ # REST コントローラー
3. ドメインレイヤーを実装する(フレームワークフリー)
ドメインレイヤーは Spring またはその他のフレームワークに対する依存性がゼロであることが必須です。
- 不変のバリューオブジェクト用に Java records を使用し、組み込みバリデーションを活用
- ビジネスロジックをサービスではなくエンティティに配置(Rich Domain Model)
- リポジトリインターフェース(ポート)をドメインレイヤーで定義
- ID の混同を防ぐために、強く型付けされた ID を使用
- 副作用の結合を解除するためにドメインイベントを実装
- 不変式を強制するためにエンティティ作成にファクトリーメソッドを使用
4. アプリケーションレイヤーを実装する
- ユースケースインターフェース(driving ポート)を
application/port/in/に作成 - 外部サービスインターフェース(driven ポート)を
application/port/out/に作成 @Serviceと@Transactionalを使用してアプリケーションサービスを実装- ドメインモデルから分離して、リクエスト/レスポンスに DTO を使用
- 成功した操作後にドメインイベントを発行
5. インフラストラクチャレイヤーを実装する(アダプター)
infrastructure/persistence/に JPA エンティティを作成- ドメインと JPA エンティティ間にマッピングするリポジトリアダプターを実装
- ドメイン-JPA 変換に MapStruct または手動マッパーを使用
- スワップ可能な実装のための条件付きビーンを設定
- ドメインロジックからインフラストラクチャの関心事を分離
6. アダプターレイヤーを実装する(REST)
adapter/rest/に REST コントローラーを作成- 実装ではなくユースケースインターフェースをインジェクト
- DTO に Bean Validation を使用
- 適切な HTTP ステータスコードとレスポンスを返す
- グローバル例外ハンドラーで例外を処理
7. ベストプラクティスを適用する
- 依存性ルール: ドメインは Spring またはその他のフレームワークに対する依存性がゼロ
- 不変バリューオブジェクト: 組み込みバリデーション付きの Java records を使用
- Rich Domain Models: ビジネスロジックをサービスではなくエンティティに配置
- Repository Pattern: ドメインでインターフェースを定義、インフラストラクチャで実装
- Domain Events: 主要な操作から副作用の結合を解除
- Constructor Injection: 最終フィールド経由の必須依存性
- DTO Mapping: ドメインモデルを API コントラクトから分離
- Transaction Boundaries:
@Transactionalをアプリケーションサービスに配置 - Factory Methods: 構築時の不変式強制に
Entity.create()を使用 - JPA エンティティを分離: ドメインエンティティを JPA エンティティから分離し、マッパーを使用
8. アーキテクチャコンプライアンスを検証する
各レイヤーの実装後、依存性ルールが守られていることを確認してください:
- Domain Layer Check:
grep -r "@Service\|@Component\|@Autowired" domain/を実行し、Spring インポートがないことを確認 - ArchUnit Test: 依存性テストを追加して、ドメインレイヤーにインフラストラクチャインポートがないことを確認:
noClasses().that().resideInPackage("..domain..") .should().accessClassesThat().resideInAnyPackage("..spring..", "..infrastructure.."); - Entity Exposure Check: JPA エンティティがドメインサービスから返されていないことを確認
- Transaction Check:
@Transactionalがアプリケーションレイヤーサービスのみにあり、ドメインにはないことを確認
9. テストを作成する
- Domain Tests: Spring コンテキストなしの純粋なユニットテスト、高速実行
- Application Tests: Mockito を使用したモック ポートのユニットテスト
- Infrastructure Tests:
@DataJpaTestと Testcontainers を使用した統合テスト - Adapter Tests:
@WebMvcTestを使用したコントローラーテスト
例
例1: ドメインレイヤー - Domain Events を持つエンティティ
// domain/model/Order.java
public class Order {
private final OrderId id;
private final List<OrderItem> items;
private Money total;
private OrderStatus status;
private final List<DomainEvent> domainEvents = new ArrayList<>();
private Order(OrderId id, List<OrderItem> items) {
this.id = id;
this.items = new ArrayList<>(items);
this.status = OrderStatus.PENDING;
calculateTotal();
}
public static Order create(List<OrderItem> items) {
validateItems(items);
Order order = new Order(OrderId.generate(), items);
order.domainEvents.add(new OrderCreatedEvent(order.id, order.total));
return order;
}
public void confirm() {
if (status != OrderStatus.PENDING) {
throw new DomainException("Only pending orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
public List<DomainEvent> getDomainEvents() {
return List.copyOf(domainEvents);
}
public void clearDomainEvents() {
domainEvents.clear();
}
}
例2: ドメインレイヤー - バリデーション付きバリューオブジェクト
// domain/model/Money.java (Value Object)
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new DomainException("Amount cannot be negative");
}
}
public static Money zero() {
return new Money(BigDecimal.ZERO, Currency.getInstance("EUR"));
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new DomainException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
例3: ドメインレイヤー - Repository ポート
// domain/repository/OrderRepository.java (Port)
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
例4: アプリケーションレイヤー - ユースケースとサービス
// application/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
OrderResponse createOrder(CreateOrderRequest request);
}
// application/dto/CreateOrderRequest.java
public record CreateOrderRequest(
@NotNull UUID customerId,
@NotEmpty List<OrderItemRequest> items
) {}
// application/service/OrderService.java
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService implements CreateOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final DomainEventPublisher eventPublisher;
@Override
public OrderResponse createOrder(CreateOrderRequest request) {
List<OrderItem> items = mapItems(request.items());
Order order = Order.create(items);
PaymentResult payment = paymentGateway.charge(order.getTotal());
if (!payment.successful()) {
throw new PaymentFailedException("Payment failed");
}
order.confirm();
Order saved = orderRepository.save(order);
publishEvents(order);
return OrderMapper.toResponse(saved);
}
private void publishEvents(Order order) {
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}
例5: インフラストラクチャレイヤー - JPA エンティティとアダプター
// infrastructure/persistence/OrderJpaEntity.java
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
@Id
private UUID id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private BigDecimal totalAmount;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItemJpaEntity> items;
}
// infrastructure/persistence/OrderRepositoryAdapter.java
@Component
@RequiredArgsConstructor
public class OrderRepositoryAdapter implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderJpaMapper mapper;
@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toEntity(order);
return mapper.toDomain(jpaRepository.save(entity));
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepository.findById(id.value()).map(mapper::toDomain);
}
}
例6: アダプターレイヤー - REST コントローラー
// adapter/rest/OrderController.java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderResponse response = createOrderUseCase.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(response.id())
.toUri();
return ResponseEntity.created(location).body(response);
}
}
例7: ドメインテスト(Spring コンテキストなし)
class OrderTest {
@Test
void shouldCreateOrderWithValidItems() {
List<OrderItem> items = List.of(
new OrderItem(new ProductId(UUID.randomUUID()), 2, new Money("10.00", EUR))
);
Order order = Order.create(items);
assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
assertThat(order.getDomainEvents()).hasSize(1);
}
}
例8: アプリケーションテスト(モックを使用したユニットテスト)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock PaymentGateway paymentGateway;
@Mock DomainEventPublisher eventPublisher;
@InjectMocks OrderService orderService;
@Test
void shouldCreateAndConfirmOrder() {
when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true, "tx-123"));
when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));
OrderResponse response = orderService.createOrder(createRequest());
assertThat(response.status()).isEqualTo(OrderStatus.CONFIRMED);
verify(eventPublisher).publish(any(OrderCreatedEvent.class));
}
}
ベストプラクティス
- Domain purity: Spring アノテーションとフレームワークインポートからドメインレイヤーを自由に保つ — 外側のレイヤーに対するゼロ依存性
- Feature-based packages: 技術的役割ではなく、ビジネス機能(
order/、customer/)で整理し、各機能に 4 つのレイヤーすべてを含める - 不変バリューオブジェクト: コンパクターに組み込みバリデーションを持つ Java records をバリューオブジェクトに使用 — 設計上不変
- Rich domain models: ビジネスロジックをアプリケーションサービスではなくエンティティとアグリゲートに配置 — サービスはオーケストレート、エンティティはカプセル化
- 常にマッピングする: MapStruct または手動マッパーを使用して JPA エンティティをドメインモデルから分離;インフラストラクチャ外で JPA エンティティを公開しない
- デカップリング用の Domain Events: 直接サービス呼び出しの代わりに、
DomainEventPublisherを使用してクロスアグリゲート副作用をデカップリング - Application layer のトランザクション境界:
@Transactionalはアプリケーションサービスのみに配置、ドメインクラスには配置しない - 不変式用のファクトリーメソッド: 構築時の不変式を強制するために
Entity.create(...)スタティックメソッドを使用 - ArchUnit で強制: テストスイートに ArchUnit テストを追加して、Spring またはインフラストラクチャインポートがドメインレイヤーに到達しないことを確認
- 強く型付けされた ID: raw
UUIDの代わりにrecord OrderId(UUID value)を使用して、アグリゲート間の ID 混同を防止
制約と警告
重大な制約
- Domain Layer Purity: ドメインクラスに Spring アノテーション(
@Entity、@Autowired、@Component)を追加しない - Dependency Direction: 依存性は内向きのみを指す(domain <- application <- infrastructure/adapter)
- Framework Isolation: すべてのフレームワーク固有コードはインフラストラクチャとアダプターレイヤーに留まる
避けるべき一般的な落とし穴
- Anemic Domain Model: ゲッター/セッターのみを持つエンティティ、サービスのロジック — ビジネスロジックをエンティティに配置
- Framework Leakage: ドメインレイヤーの
@Entity、@Autowired— ドメインをフレームワークフリーに保つ - Lazy Loading Issues: JPA エンティティをドメインモデルを通じて公開 — マッパーを使用して変換
- Circular Dependencies: ドメインアグリゲート間 — 直接参照の代わりに ID を使用
- Missing Domain Events: クロスアグリゲート通信のための直接サービス呼び出しの代わりにイベント
- Repository Misplacement: インフラストラクチャでのリポジトリインターフェース定義 — ドメインに属する
- DTO Bypass: API で直接ドメインエンティティを公開 — 外部コントラクト用に常に DTO を使用
パフォーマンス考慮事項
- 遅延ロードの問題を避けるために、JPA エンティティをドメインモデルから分離
- クエリ操作には読み取り専用トランザクションを使用
- 複雑な読み取り/書き込みシナリオの場合は CQRS を検討
参考資料
references/java-clean-architecture.md- Java 固有のパターン(records、sealed classes、強く型付けされた ID)references/spring-boot-implementation.md- Spring Boot 統合(DI パターン、JPA マッピング、トランザクション管理)
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- giuseppe-trisciuoglio
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/giuseppe-trisciuoglio/developer-kit / ライセンス: MIT
関連スキル
nano-banana-2
inference.sh CLIを通じてGoogle Gemini 3.1 Flash Image Preview(Nano Banana 2)で画像を生成します。テキストから画像を生成する機能、画像編集、最大14枚の複数画像入力、Google Searchグラウンディング機能に対応しています。トリガーワード:「nano banana 2」「nanobanana 2」「gemini 3.1 flash image」「gemini 3 1 flash image preview」「google image generation」
octocode-slides
洗練されたマルチファイル形式のHTMLプレゼンテーションを生成します。6段階のフロー(概要 → リサーチ → アウトライン → デザイン → 実装 → レビュー)で構成されています。各スライドは独立したHTMLファイルとなり、iframeで読み込まれます。「スライドを作成してほしい」「プレゼンテーションを作ってほしい」「HTMLスライドを生成してほしい」「デックを構築してほしい」といった依頼や、ノート・ドキュメント・コードを洗練されたプレゼンテーションに変換する際に使用できます。
gpt-image2-ppt
OpenAIのgpt-image-2を使用して、視覚的に優れたPPTスライドを生成します。Spatial Glass、Tech Blue、Editorial Monoなど10種類のキュレーション済みスタイルに対応し、ユーザーが提供したPPTXファイルを模倣するテンプレートクローンモードも搭載しています。HTMLビューアと16:9形式のPPTXファイルを出力します。プレゼンテーション、スライド、ピッチデック、投資家向けPPT、雑誌風PPTの作成依頼などで活用してください。
nano-banana
Nano Banana PRO(Gemini 3 Pro Image)およびNano Banana(Gemini 2.5 Flash Image)を使用したAI画像生成機能です。以下の場合に活用できます:(1)テキストプロンプトからの画像生成、(2)既存画像の編集、(3)インフォグラフィックス、ロゴ、商品写真、ステッカーなどのプロフェッショナルなビジュアルアセット制作、(4)複数画像での人物キャラクターの一貫性保持、(5)正確なテキスト描画を含む画像生成、(6)AI生成ビジュアルが必要なあらゆるタスク。「画像を生成」「画像を作成」「写真を作る」「ロゴをデザイン」「インフォグラフィックスを作成」「AI画像」「nano banana」またはその他の画像生成リクエストをトリガーとして機能します。
oiloil-ui-ux-guide
モダンでクリーンなUI/UXガイダンス・レビュースキルです。新機能や既存システム(Webアプリ)に対して、実行可能なUI/UX改善提案、デザイン原則、デザインレビューチェックリストが必要な場合に活用できます。CRAP(コントラスト・反復・配置・近接)をベースに、タスクファーストなUX、情報設計、フィードバック・システムステータス、一貫性、affordances、エラー防止・復旧、認知負荷を重視します。モダンミニマルスタイル(クリーン・余白・タイポグラフィ主導)を強制し、不要なテキストを削減、アイコンとしての絵文字を禁止し、統一されたアイコンセットから直感的で洗練されたアイコンを推奨します。
axiom-hig-ref
Apple Human Interface Guidelines リファレンス — 色(セマンティックカラー、カスタムカラー、パターン)、背景(マテリアル階層、ダイナミック背景)、タイポグラフィ(標準スタイル、カスタムフォント、Dynamic Type)、SF Symbols(レンダリングモード、色、多言語対応)、ダークモード、アクセシビリティ、プラットフォーム固有の考慮事項を網羅したガイドラインです。