【アーキテクチャ】モジュラモノリス - j-komatsu/myCheatSheet GitHub Wiki
モジュラモノリス(Modular Monolith) は、1つのプロセス内で複数の独立したモジュールを持つアーキテクチャパターンです。
| アーキテクチャ | デプロイ単位 | モジュール分離 | 通信方式 |
|---|---|---|---|
| モノリス | 1つ | なし(密結合) | メソッド呼び出し |
| モジュラモノリス | 1つ | 明確な境界 | インターフェース経由 |
| マイクロサービス | 複数 | プロセス分離 | HTTP/gRPC/メッセージング |
- 1プロセス内での独立モジュール化:各モジュールは明確な境界を持つ
- DDD(ドメイン駆動設計)との親和性:Bounded Contextをモジュール単位で表現
- 段階的移行:モノリス → モジュラモノリス → マイクロサービスへの自然な進化パス
| 観点 | モノリス | モジュラモノリス | マイクロサービス |
|---|---|---|---|
| 開発速度 | 初期◎ / 中期△ | ◎ | 初期△ / 中期◎ |
| デプロイ複雑性 | ◎ | ◎ | △ |
| チーム独立性 | × | ○ | ◎ |
| テスト容易性 | ○ | ◎ | △ |
| 運用コスト | ◎ | ◎ | △ |
| スケーラビリティ | △ | △ | ◎ |
| 技術スタック統一 | 必須 | 必須 | 自由 |
✅ モジュラモノリスが最適
- チーム規模:5〜20人程度
- ドメイン複雑度:中〜高
- デプロイ要件:週次〜日次デプロイ
- インフラ習熟度:中程度
- チーム規模:20人以上
- 独立デプロイ要件:各チームが1日複数回デプロイ
- スケール要件:一部機能のみ大規模スケールが必要
my-application/
├── order-module/
│ ├── domain/
│ │ ├── Order.java
│ │ └── OrderRepository.java (interface)
│ ├── application/
│ │ └── OrderService.java
│ └── infrastructure/
│ └── JpaOrderRepository.java
├── payment-module/
│ ├── domain/
│ │ ├── Payment.java
│ │ └── PaymentRepository.java
│ ├── application/
│ │ └── PaymentService.java
│ └── infrastructure/
│ └── JpaPaymentRepository.java
├── api/
│ ├── OrderController.java
│ └── PaymentController.java
└── build.gradle
// ルートプロジェクト
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
}
}
// settings.gradle
include 'order-module'
include 'payment-module'
include 'api'// PaymentService.java
public class PaymentService {
@Autowired
private OrderRepository orderRepository; // 他モジュールの実装に直接依存
}// order-module/domain/OrderQuery.java (公開インターフェース)
public interface OrderQuery {
Optional<Order> findById(Long orderId);
}
// payment-module/application/PaymentService.java
public class PaymentService {
private final OrderQuery orderQuery; // インターフェースに依存
public PaymentService(OrderQuery orderQuery) {
this.orderQuery = orderQuery;
}
}// order-module/module-info.java
module order.module {
requires spring.context;
requires spring.data.jpa;
exports com.example.order.domain; // 公開パッケージ
exports com.example.order.application; // 公開パッケージ
// infrastructure層は非公開(外部から直接アクセス不可)
}graph LR
A[Traditional Monolith] -->|Refactor| B[Modular Monolith]
B -->|Extract| C[Microservices]
subgraph "Modular Monolith"
B1[Order Module]
B2[Payment Module]
B3[User Module]
B1 -.->|interface| B2
B2 -.->|interface| B3
end
subgraph "Microservices"
C1[Order Service]
C2[Payment Service]
C3[User Service]
C1 -->|HTTP/gRPC| C2
C2 -->|HTTP/gRPC| C3
end
graph TD
A[Controller/API Layer] --> B[Application Layer]
B --> C[Domain Layer]
C --> D[Infrastructure Layer]
D -.->|implements| C
style C fill:#f9f,stroke:#333,stroke-width:4px
style D fill:#bbf,stroke:#333,stroke-width:2px
// order-module/config/OrderModuleConfig.java
@Configuration
@ComponentScan(basePackages = "com.example.order")
public class OrderModuleConfig {
@Bean
public OrderService orderService(OrderRepository orderRepository) {
return new OrderService(orderRepository);
}
@Bean
public OrderQuery orderQuery(OrderRepository orderRepository) {
return new OrderQueryImpl(orderRepository); // 公開インターフェースの実装
}
}// order-module/domain/Order.java
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerId;
private BigDecimal totalAmount;
private OrderStatus status;
// ビジネスロジックはドメイン層に集約
public void confirm() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("Order must be in PENDING status");
}
this.status = OrderStatus.CONFIRMED;
}
}// order-module/application/OrderService.java
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient; // 外部モジュールとの連携
public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
}
public void placeOrder(CreateOrderCommand command) {
Order order = new Order(command.getCustomerId(), command.getItems());
orderRepository.save(order);
// 他モジュールとの連携はインターフェース経由
paymentClient.initiatePayment(order.getId(), order.getTotalAmount());
}
}| アーキテクチャパターン | モジュラモノリスとの関係 |
|---|---|
| クリーンアーキテクチャ | 各モジュール内部で適用可能 |
| レイヤードアーキテクチャ | モジュール内の基本構造として使用 |
| ヘキサゴナルアーキテクチャ | Port/Adapter パターンをモジュール境界に適用 |
| マイクロサービス | モジュラモノリスからの段階的移行先 |
| CQRS | モジュール内でコマンド/クエリ分離可能 |
Phase 1: レガシーモノリス
└─ すべてが密結合、レイヤ構造も曖昧
Phase 2: モジュラモノリス化
└─ ドメイン境界でモジュール分割
└─ インターフェース経由での依存関係明確化
└─ モジュール単位でのテスト可能
Phase 3: マイクロサービス化(必要に応じて)
└─ 独立デプロイが必要なモジュールから順次分離
└─ プロセス間通信への変更(同期/非同期)
❌ 問題
// payment-module が order-module の実装クラスに直接依存
import com.example.order.infrastructure.JpaOrderRepository;✅ 解決策
- 公開インターフェースのみをexportsで公開
- 依存逆転の原則(DIP)を適用
- ArchUnitなどで依存関係をテストで検証
@ArchTest
static final ArchRule moduleDependencyRule =
noClasses()
.that().resideInPackage("..payment..")
.should().dependOnClassesThat().resideInPackage("..order.infrastructure..");❌ 問題
-
common-utilsモジュールが肥大化し、すべてのモジュールが依存
✅ 解決策
- 真に共通的なもの(DateUtil等)のみ配置
- ドメイン固有のロジックは各モジュール内に
- Shared Kernelパターン(DDD)を最小限に適用
❌ 問題
Order Module → Payment Module → User Module → Order Module
✅ 解決策
- イベント駆動アーキテクチャの導入(Spring ApplicationEvent)
- 共通インターフェースを別モジュールに分離
- 依存方向を一方向に統一(上位モジュール → 下位モジュール)
// order-module内でイベント発行
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void confirmOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.confirm();
// 他モジュールに直接依存せず、イベントで通知
eventPublisher.publishEvent(new OrderConfirmedEvent(orderId));
}
}
// payment-module内でイベント受信
@Component
public class OrderEventListener {
@EventListener
public void handleOrderConfirmed(OrderConfirmedEvent event) {
// 支払い処理を開始
}
}✅ 解決策
// モジュール単位でのテスト
@SpringBootTest(classes = OrderModuleConfig.class)
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean
private PaymentClient paymentClient; // 外部モジュールはモック化
@Test
void testPlaceOrder() {
// テスト実装
}
}-
早すぎるマイクロサービス化を避ける
- 最初からマイクロサービスにすると運用コストが高い
- まずはモジュラモノリスで境界を明確にする
-
モジュール境界 = Bounded Context
- DDDの概念を活用し、ビジネスドメイン単位で分割
- ユビキタス言語をモジュール内で統一
-
依存方向の徹底管理
- Domain層は他の何にも依存しない
- Infrastructure層がDomain層の抽象に依存(依存性逆転)
-
段階的な進化
モノリス → モジュラモノリス → (必要なら)マイクロサービス
| チーム規模 | 推奨アーキテクチャ |
|---|---|
| 1〜5人 | シンプルなモノリス |
| 5〜20人 | モジュラモノリス |
| 20人以上 | マイクロサービスも検討 |
- Bounded Context(境界付けられたコンテキスト): DDDにおけるモデルの適用範囲
- Aggregate(集約): ドメインオブジェクトの整合性境界
- Module Boundary(モジュール境界): モジュール間の依存関係を制御する境界
- Port/Adapter: ヘキサゴナルアーキテクチャのパターン(外部との接点を抽象化)
- 📖 Vernon著『実践ドメイン駆動設計』
- 📖 Evans著『ドメイン駆動設計』
- 🌐 Kamil Grzybek - Modular Monolith
- 🌐 Simon Brown - Modular Monoliths
課題1: 既存のシンプルなECサイト(モノリス)を以下の3モジュールに分割してみましょう
-
product-module(商品カタログ) -
order-module(注文処理) -
customer-module(顧客管理)
課題2: モジュール間の循環依存が発生した場合、イベント駆動で解決する実装例を作成してみましょう
- 【アーキテクチャ】マイクロサービスアーキテクチャ - モジュラモノリスからの移行先
- 【アーキテクチャ】イベント駆動アーキテクチャ - モジュール間疎結合の実現手法
- 【アーキテクチャ】CQRS(コマンドクエリ責任分離) - モジュール内での適用パターン
- 【アーキテクチャ】ドメイン駆動設計(DDD) - モジュール境界の設計理論
- 【アーキテクチャ】クリーンアーキテクチャ - モジュール内部の構造設計
📝 最終更新: 2025-10-25