spring-modulith について
Spring Modulith を用いたモジュラーモノリス設計を支援し、モジュール境界、公開 API、イベントベースの通信を扱うスキルです。Spring アプリケーションを境界づけられたコンテキストに整理し、アーキテクチャやモジュール間の連携を確認する開発者向けです。
主な機能
- 01モジュール境界と許可された依存関係
- 02公開 API と内部パッケージの分離
- 03イベントベースのモジュール間通信
- 04アーキテクチャ検証とモジュールテスト
- 05イベント永続化と再試行の考慮
活用シーン
- 01Spring アプリケーションのモジュラーモノリス化
- 02モジュール内部へのアクセス制限
- 03アプリケーションイベントによるモジュール連携
- 04モジュール境界と循環依存の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-modulithインストール後、Claude Code で/spring-modulithを入力して読み込まれたか確認します。
同梱ファイルadvanced.md
詳細
Spring Modulith
完全なリファレンス:イベントの外部化(Outbox)、モジュール API の公開、@ApplicationModuleTest、シナリオテスト、アーキテクチャ検証、可観測性、段階的な分解については、advanced.md を参照してください。
概要
┌─────────────────────────────────────────────────────────────────┐
│ Spring Modulith Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Order │ │ Payment │ │ Inventory │ │
│ │ Module │──▶│ Module │◀──│ Module │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ order/ │ │ payment/ │ │ inventory/ │ │
│ │ ├─ api/ │ │ ├─ api/ │ │ ├─ api/ │ │
│ │ │ (public) │ │ │ (public) │ │ │ (public) │ │
│ │ └─ internal/ │ │ └─ internal/ │ │ └─ internal/ │ │
│ │ (private) │ │ (private) │ │ (private) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ Event Bus (Async) │
│ │
└─────────────────────────────────────────────────────────────────┘
クイックスタート
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
src/main/java/com/example/ecommerce/
├── EcommerceApplication.java # Root package
├── order/ # Order module
│ ├── Order.java # Public API
│ ├── OrderService.java # Public API
│ ├── OrderCreatedEvent.java # Public event
│ └── internal/ # Internal implementation
│ ├── OrderRepository.java
│ └── OrderValidator.java
├── payment/ # Payment module
│ ├── PaymentService.java
│ └── internal/
└── shared/ # Shared kernel (minimal!)
└── Money.java
モジュール構造
// Package-info to document module
// order/package-info.java
@org.springframework.modulith.ApplicationModule(
displayName = "Order Management",
allowedDependencies = {"payment", "inventory::InventoryService"}
)
package com.example.ecommerce.order;
// Public API (root package)
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher events;
public Order createOrder(CreateOrderRequest request) {
Order order = Order.create(request.customerId(), request.items());
order = orderRepository.save(order);
// Publish event for other modules
events.publishEvent(new OrderCreatedEvent(order.getId(), order.getTotal()));
return order;
}
public void confirmOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
orderRepository.save(order);
events.publishEvent(new OrderConfirmedEvent(orderId));
}
}
// Public event
public record OrderCreatedEvent(Long orderId, Money total) {}
// Internal implementation (not accessible from other modules)
// order/internal/OrderRepository.java
@Repository
interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerId(Long customerId);
}
イベントを介したモジュール間通信
// Payment module listens to Order module events
// payment/internal/OrderEventHandler.java
@Component
@RequiredArgsConstructor
@Slf4j
class OrderEventHandler {
private final PaymentService paymentService;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
log.info("Order created: {}, processing payment", event.orderId());
paymentService.initiatePayment(event.orderId(), event.total());
}
}
// payment/PaymentService.java
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentRepository paymentRepository;
private final ApplicationEventPublisher events;
public void initiatePayment(Long orderId, Money amount) {
Payment payment = Payment.create(orderId, amount);
payment = paymentRepository.save(payment);
processPaymentAsync(payment);
}
@Async
void processPaymentAsync(Payment payment) {
try {
payment.confirm();
paymentRepository.save(payment);
events.publishEvent(new PaymentConfirmedEvent(payment.getOrderId(), payment.getId()));
} catch (PaymentFailedException e) {
payment.fail(e.getMessage());
paymentRepository.save(payment);
events.publishEvent(new PaymentFailedEvent(payment.getOrderId(), e.getMessage()));
}
}
}
// Order module reacts to Payment events
// order/internal/PaymentEventHandler.java
@Component
@RequiredArgsConstructor
class PaymentEventHandler {
private final OrderService orderService;
@EventListener
public void onPaymentConfirmed(PaymentConfirmedEvent event) {
orderService.confirmOrder(event.orderId());
}
@EventListener
public void onPaymentFailed(PaymentFailedEvent event) {
orderService.cancelOrder(event.orderId(), event.reason());
}
}
ベストプラクティス
モジュール設計
// ✅ DO: Expose only what's needed
@ApplicationModule(allowedDependencies = {"shared"})
package com.example.ecommerce.order;
// ✅ DO: Communicate via events
events.publishEvent(new OrderCreatedEvent(orderId));
// ✅ DO: Use records for immutable events
public record OrderCreatedEvent(Long orderId, Money total) {}
// ❌ DON'T: Circular dependencies
// order → payment → order // WRONG!
// ❌ DON'T: Expose repositories
public interface OrderRepository { } // Should not be public
// ❌ DON'T: Direct access to internal
@Autowired
OrderValidator validator; // From another module - WRONG!
イベント設計
// ✅ DO: Events with all necessary data
public record OrderCreatedEvent(
Long orderId,
Long customerId,
Money total,
List<OrderItem> items,
Instant createdAt
) {}
// ❌ DON'T: Events requiring callback
public record OrderCreatedEvent(Long orderId) {}
// Consumer must call orderService.getOrder(orderId) - WRONG!
ベストプラクティス一覧
| 推奨事項 | 避けるべきこと |
|---|---|
| 1 つのモジュール=1 つの境界づけられたコンテキスト | 無関係な関心事を混在させる |
| ルートパッケージに公開 API を配置する | 内部クラスを公開する |
実装を internal/ に配置する | 外部から内部にアクセスする |
| イベントを介して通信する | モジュール間で直接呼び出す |
| 不変イベント(record)を使用する | 可変イベントオブジェクトを使用する |
本番環境チェックリスト
- モジュールの境界を定義する
- 内部パッケージを適切にスコープ設定する
- イベントベースの通信を使用する
- アーキテクチャ検証テストを用意する
- イベントの永続化を設定する
- 失敗したイベントの再試行メカニズムを用意する
- ドキュメントを生成する
- 循環依存がない
- 共有カーネルを最小限にする
このスキルを使用しない場合
- シンプルなアプリケーション - 不要な複雑さが生じます
- 既存のマイクロサービス - すでに分解されています
- 密結合したモノリス - まず大幅なリファクタリングが必要です
- 小規模なチーム - 厳密な境界が不要な場合があります
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| 循環依存 | モジュール同士が相互に参照する | イベントまたは共有カーネルを使用する |
| 内部クラスの公開 | パッケージ構造が不適切 | internal/ パッケージに移動する |
| イベントが公開されない | トランザクションが不足している | @Transactional を確認する |
| イベントの消失 | 永続化されていない | spring-modulith-events-jpa を使用する |
| コールバックイベント | イベント処理にコールバックが必要 | 必要なデータをすべてイベントに含める |
| リポジトリの公開 | 密結合になる | リポジトリを内部にとどめる |
すぐにできるトラブルシューティング
| 問題 | 診断 | 修正 |
|---|---|---|
| 循環依存 | modules.verify() を実行する | イベントを使用するようリファクタリングする |
| 内部アクセス違反 | パッケージ構造を確認する | クラスを適切な場所に移動する |
| イベントを受信できない | リスナーを確認する | @EventListener アノテーションを確認する |
| 分離テストが失敗する | 依存関係を確認する | 適切な BootstrapMode を使用する |
| イベントの公開に失敗する | トランザクションを確認する | @Transactional が存在することを確認する |
リファレンスドキュメント
コメント
Markdown 対応