はじめに: ハイブリッド アーキテクチャとしてのモジュラー モノリス
前回の記事では、マイクロサービスの危機とそれが示す警告の兆候を分析しました。 配布が問題になったとき。今こそ解決策を探るときです。 モジュラーモノリス、単一の操作のシンプルさを組み合わせたアーキテクチャです。 マイクロサービスに特有のモジュール性と論理的独立性を備えた展開可能なプロセス。
モジュラーモノリスは下方への妥協ではなく、 意識的なアーキテクチャの選択 これにより、両方の長所を最大限に活かすことができます。この記事では、デザイン方法について説明します。 モジュールの境界、内部 API の定義、およびフレームワークの仕組み ばね弾性率 これらは、長期にわたってアーキテクチャの整合性を維持するのに役立ちます。
この記事で学べること
- モジュラーモノリスアーキテクチャの基本原理
- 明確で保守可能なモジュール境界を定義する方法
- 内部 API の役割とモジュール間のコントラクト
- Spring Modulith: コンパイル時に境界を適用するフレームワーク
- 構造化比較: モノリス、モジュラーモノリス、マイクロサービス
- 図とコードを含むリファレンス アーキテクチャ
モジュラーモノリスの基本原理
モジュール式モノリスは、4 つのアーキテクチャ原則によって従来のモノリスと区別されます。 コードの構造とコンポーネント間の相互作用を制御します。
1. 単一の展開可能な複数の論理モジュール
アプリケーションはコンパイルされ、次のようにデプロイされます。 単一のアーティファクト (JAR 1 つ、WAR 1 つ、 バイナリ)ですが、内部的には独立した論理モジュールに編成されています。各モジュールには独自のモジュールがあります パッケージ、独自のクラス、および明確に定義されたパブリック API サーフェス。モジュールは通信します パブリック インターフェイスのみを介してアクセスし、他のモジュールの内部クラスにはアクセスしません。
2. カプセル化と情報隠蔽
各モジュールは、その内部実装をパブリック インターフェイスの背後に隠します。クラス ドメイン、リポジトリ、内部サービスは 外部からアクセスできない。 API、DTO、およびイベントのみが公開されます。これにより、モジュールは次のことが可能になります。 消費者を損なうことなく内部的に進化します。
3. データの所有権
各モジュール 独占所有者 あなたのデータの。物理データベースであっても、 共有できる場合、各モジュールは独自のリポジトリを介してのみ独自のテーブルにアクセスします。 どのモジュールも別のモジュールのテーブルを直接読み書きすることはできません。テーブルを通過する必要があります。 パブリック API を通じて。
4. 明示的なコミュニケーション
モジュール間の相互作用は次のように行われます。 明示的なチャネル: メソッド呼び出し インターフェイス、ドメイン イベント、またはコマンド/クエリを通じて。隠れた依存関係もありません 他のモジュールのデータベースに直接アクセスします。すべての依存関係が表示され、追跡可能です。
主な違い
従来のモノリスでは、どのクラスも他のクラスを呼び出すことができます。モジュール式で モノリス、モジュールは通信可能 パブリックインターフェース経由のみ。これ 制限と、モジュールをマイクロサービスとして抽出できるようにするものについて説明します。
リファレンスアーキテクチャ
モジュール式モノリスが実際にどのように構造化されているかを見てみましょう。次の例はアプリケーションを示しています 4 つのモジュールを備えた e コマース: 注文, カタログ, ユーザー e 支払い.
// Struttura del progetto modular monolith
// com.ecommerce/
// ├── order/
// │ ├── api/
// │ │ ├── OrderModuleApi.java (interfaccia pubblica)
// │ │ ├── OrderDto.java (DTO pubblico)
// │ │ └── OrderCreatedEvent.java (evento pubblico)
// │ └── internal/
// │ ├── OrderService.java (logica privata)
// │ ├── OrderRepository.java (accesso dati privato)
// │ └── Order.java (entità privata)
// ├── catalog/
// │ ├── api/
// │ │ ├── CatalogModuleApi.java
// │ │ └── ProductDto.java
// │ └── internal/
// │ ├── CatalogService.java
// │ └── Product.java
// ├── user/
// │ ├── api/
// │ │ └── UserModuleApi.java
// │ └── internal/
// │ └── UserService.java
// └── payment/
// ├── api/
// │ └── PaymentModuleApi.java
// └── internal/
// └── PaymentService.java
内部 API の定義
各モジュールは、 公契約 フォームの。
このインターフェイスは、他のモジュールへの唯一のアクセス ポイントです。実装は隠蔽されています
パッケージの中に internal.
// API pubblica del modulo Order
package com.ecommerce.order.api;
public interface OrderModuleApi {
/**
* Crea un nuovo ordine.
* Pubblica OrderCreatedEvent al completamento.
*/
OrderDto createOrder(CreateOrderCommand command);
/**
* Recupera un ordine per ID.
* @throws OrderNotFoundException se l'ordine non esiste
*/
Optional<OrderDto> findById(UUID orderId);
/**
* Lista ordini di un utente con paginazione.
*/
Page<OrderDto> findByUserId(UUID userId, Pageable pageable);
/**
* Annulla un ordine esistente.
* Pubblica OrderCancelledEvent al completamento.
*/
void cancelOrder(UUID orderId);
}
// DTO pubblico - solo dati, nessuna logica di dominio
public record OrderDto(
UUID id,
UUID userId,
List<OrderItemDto> items,
BigDecimal total,
OrderStatus status,
Instant createdAt
) {}
モジュールの内部実装
モジュールの実装は完全に隠蔽されます。パッケージ内のクラス internal
外部からはアクセスできません。これにより、実装を自由にリファクタリングできます
消費者に影響を与えることなく。
// Implementazione privata del modulo Order
package com.ecommerce.order.internal;
@Service
class OrderServiceImpl implements OrderModuleApi {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
private final CatalogModuleApi catalogModule; // dipendenza da altro modulo
OrderServiceImpl(OrderRepository orderRepository,
ApplicationEventPublisher eventPublisher,
CatalogModuleApi catalogModule) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
this.catalogModule = catalogModule;
}
@Override
@Transactional
public OrderDto createOrder(CreateOrderCommand command) {
// Verifica prodotti tramite API pubblica del modulo Catalog
List<ProductDto> products = command.getItemIds().stream()
.map(catalogModule::findById)
.map(opt -> opt.orElseThrow(ProductNotFoundException::new))
.toList();
// Crea l'entità di dominio (classe privata)
Order order = Order.create(command.getUserId(), products);
orderRepository.save(order);
// Pubblica evento per altri moduli interessati
eventPublisher.publishEvent(
new OrderCreatedEvent(order.getId(), order.getUserId())
);
return order.toDto();
}
}
Spring Modulith: コンパイル時の境界
ばね弾性率 そして、次のツールを提供する公式の Spring フレームワーク Spring Boot でモジュラー モノリスを構造化し、検証し、文書化します。その特徴 メインと能力 コンパイル時にモジュール間の境界をチェックする、 パッケージ間の不正アクセスを防止します。
スプリングモジュリス構成
<!-- pom.xml - Dipendenze Spring Modulith -->
<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>
境界確認試験
Spring Modulith は、モジュール間のアクセス ルールを検証する自動テストを提供します。 尊敬されています。モジュールが別のモジュールの内部クラスにアクセスすると、テストは失敗します。
// Test che verifica l'integrita delle boundaries
@Test
void verifyModularStructure() {
ApplicationModules modules = ApplicationModules.of(
EcommerceApplication.class
);
// Verifica che nessun modulo acceda a classi interne
// di un altro modulo
modules.verify();
// Genera documentazione automatica dei moduli
new Documenter(modules)
.writeModulesAsPlantUml()
.writeIndividualModulesAsPlantUml();
}
// Output del test in caso di violazione:
// Module 'payment' depends on non-exposed type
// com.ecommerce.order.internal.Order
// from module 'order'
Spring Modulith を使用したイベントベースの通信
Spring Modulith は、イベントベースのモジュール間通信をネイティブにサポートします。イベント
経由で公開されます ApplicationEventPublisher そして消費される
@ApplicationModuleListener、生産者と消費者の間の分離を確保します。
// Modulo Order: pubblica un evento
@Service
class OrderServiceImpl implements OrderModuleApi {
private final ApplicationEventPublisher events;
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
events.publishEvent(new OrderCreatedEvent(
order.getId(), order.getUserId(), order.getTotal()
));
return order.toDto();
}
}
// Modulo Payment: reagisce all'evento
@Service
class PaymentEventHandler {
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
// Processa il pagamento quando un ordine viene creato
paymentService.processPayment(
event.userId(), event.total()
);
}
}
// Modulo Notification: reagisce allo stesso evento
@Service
class NotificationEventHandler {
@ApplicationModuleListener
void onOrderCreated(OrderCreatedEvent event) {
notificationService.sendOrderConfirmation(event.userId());
}
}
比較: モノリス vs モジュラー モノリス vs マイクロサービス
モジュラーモノリスが建築パノラマのどこに位置するかを理解するために、比較してみましょう 主要な次元に関する 3 つのアーキテクチャ:
運用の複雑さ
- 従来のモノリス: 低い。 1 つの展開、1 つのプロセス、一元化されたログ。ただし、内部コードが複雑になる可能性があります
- モジュラーモノリス: 低い。構造化され保守可能な内部コードによる、モノリスと同じ運用上の利点
- マイクロサービス: 高い。 N パイプライン、N デプロイメント、分散トレース、サービス メッシュ、ネットワーク ポリシー
スケーラビリティ
- 従来のモノリス: 水平 (複数のインスタンス)。 95% の場合に十分です
- モジュラーモノリス: 水平 + 重要なモジュールを独立したサービスとして抽出する機能
- マイクロサービス:サービスごとに独立。非常に非対称な荷重の場合にのみ必要
チームの自主性
- 従来のモノリス: 限定。明確な境界線がなく、全員が同じコードベースで作業します
- モジュラーモノリス: 適度。明確なインターフェイス、共有展開を備えたモジュールごとのチーム
- マイクロサービス: 高い。完全に独立したチーム、独立した展開
コスト
- 従来のモノリス:最低限。シンプルなインフラストラクチャ
- モジュラーモノリス:最小限~中程度。モノリスと同じインフラストラクチャコスト
- マイクロサービス: 高い。業界の推定によると、モノリスと比較して 6 倍
モジュラーモノリスのスイートスポット
モジュール式モノリスは、 最適なポイント 大多数にとって 組織: モノリスの運用コストを低く抑え、必要なモジュール性を提供します 中規模のチーム向けであり、将来的にマイクロサービスを抽出する機能を保持します。 データがそれを正当化する場合。
モジュールをマイクロサービスとして抽出する場合
モジュラーモノリスは必ずしも終点ではありません。そしてかなりの 出発点 固体 必要に応じてそこからマイクロサービスを抽出します。しかし、いつが適切な時期なのかをどうやって知ることができるのでしょうか?
モジュールの抽出を正当化する兆候は次のとおりです。
- 非対称スケーリング: このモジュールは他のモジュールより 10 倍のリソースを必要とします
- 専任チーム: 5 人以上のチームがそのモジュールのみで作業し、独立したデプロイメントが必要です
- さまざまなテクノロジースタック: モジュールは別の言語またはランタイムから恩恵を受けるでしょう
- リリース頻度の違い: モジュールは毎日のリリースを必要としますが、システムの残りの部分は毎週リリースされます。
- 分離失敗: モジュールのバグによりシステム全体がクラッシュする可能性があります
実際の実装: 最初のステップ
既存のモノリスがあり、モジュラー モノリスへの移行を開始したい場合は、最初の
ステップと コードを機能ごとにパッケージに整理する 技術層ごとではなく。
別れる代わりに controller, service, repository、
で区切る order, catalog, user.
// PRIMA: organizzazione per layer tecnico (sbagliato)
// com.app/
// ├── controller/
// │ ├── OrderController.java
// │ ├── UserController.java
// │ └── CatalogController.java
// ├── service/
// │ ├── OrderService.java
// │ ├── UserService.java
// │ └── CatalogService.java
// └── repository/
// ├── OrderRepository.java
// ├── UserRepository.java
// └── CatalogRepository.java
// DOPO: organizzazione per feature/modulo (corretto)
// com.app/
// ├── order/
// │ ├── api/OrderModuleApi.java
// │ └── internal/
// │ ├── OrderController.java
// │ ├── OrderService.java
// │ └── OrderRepository.java
// ├── user/
// │ ├── api/UserModuleApi.java
// │ └── internal/
// │ ├── UserController.java
// │ ├── UserService.java
// │ └── UserRepository.java
// └── catalog/
// ├── api/CatalogModuleApi.java
// └── internal/
// ├── CatalogController.java
// ├── CatalogService.java
// └── CatalogRepository.java
次のステップ
この記事では、モジュール式モノリスの基本原理を定義し、その仕組みを説明しました。 明確な境界を持ってコードを構造化します。次の記事では、それぞれの側面についてさらに詳しく説明します。
- 第3条: ドメイン駆動設計を使用して境界のあるコンテキストを特定し、モジュール境界を定義する方法
- 第4条: 共有スキーマと個別スキーマのパターンを使用して、モジュール式モノリスのデータベースを設計する方法
- 第5条: モジュール間の通信パターン: 同期、非同期、イベント駆動
次の記事
次の記事では、 ドメイン駆動設計 そしてその使い方 を特定するために 境界のあるコンテキスト アプリケーションの。コンセプトを見ていきます ユビキタス言語、コンテキスト マッピング、集合デザイン、およびそれらを境界に変換する方法など。 コード内の具体的なモジュール。







