はじめに: モジュール性の基盤としての DDD
Il ドメイン駆動設計 (DDD) これはソフトウェア モデリングへの単なる 1 つのアプローチではありません。 モジュール式モノリス内の正しいモジュール境界を特定するための最も強力なツールです。 DDD がないと、モジュール間の境界は技術的な基準ではなく技術的な基準に基づいて恣意的になります。 実際のドメイン境界上で。その結果、ビジネスを反映しない脆弱なアーキテクチャが作成されます。
この記事では、モジュール化に適用される DDD の基本概念について説明します。 境界のあるコンテキスト, ユビキタス言語, コンテキストマッピング、 そして 集合設計。これらの概念をモジュール境界に変換する方法を見ていきます。 具体的なものと、それらを Java/Spring Boot コードに実装する方法。
この記事で学べること
- DDD の基礎: エンティティ、値オブジェクト、集約、および境界付きコンテキスト
- アプリケーションの境界付きコンテキストを識別する方法
- ユビキタス言語: 各モジュールには独自の語彙があるため
- コンテキスト マッピング: 破損防止レイヤー、共有カーネル、顧客/サプライヤー
- 集約設計: 一貫したトランザクションの鍵
- 境界付きコンテキストを Java パッケージ構造に変換する方法
- コンウェイの法則: チームとアーキテクチャの調整
モジュラーモノリスの DDD の基礎
ドメイン駆動設計では、ソフトウェアをモデリングするための正確な語彙が導入されています。 ビジネスドメイン。基本的な構成要素を見てみましょう。
実在物
あ'実在物 そして、時間の経過とともに持続する独自のアイデンティティを持つ支配の対象です。
2 つのエンティティは、属性の値に関係なく、同じ ID を持つ場合は同等です。
例: Order, User, Product.
値オブジェクト
Un 値オブジェクト およびその属性によって定義された、アイデンティティのない不変オブジェクト
自分のもの。同じ属性を持つ 2 つの値オブジェクトは同一です。例: Money,
Address, EmailAddress.
集合体
Un 集約された エンティティと値オブジェクトのクラスターは、その単位として扱われます。 変更操作。集合体には、 ルートエンティティ アクセスポイントとして機能します そして内部不変式の整合性を保証します。トランザクションは決して i を越えてはなりません 集合体の境界。
// Esempio di Aggregate: Order
// Order e la root entity dell'aggregate
// OrderItem e Money sono parte dell'aggregate
public class Order {
private final UUID id; // identità dell'entità
private UUID userId;
private List<OrderItem> items; // entità figlie
private Money total; // value object
private OrderStatus status;
private Instant createdAt;
// Factory method - unico modo per creare un Order
public static Order create(UUID userId, List<ProductDto> products) {
Order order = new Order();
order.id = UUID.randomUUID();
order.userId = userId;
order.items = products.stream()
.map(p -> OrderItem.fromProduct(p))
.toList();
order.total = Money.sum(
order.items.stream().map(OrderItem::getPrice).toList()
);
order.status = OrderStatus.CREATED;
order.createdAt = Instant.now();
return order;
}
// Le invarianti sono protette all'interno dell'aggregate
public void cancel() {
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException(
"Cannot cancel a shipped order"
);
}
this.status = OrderStatus.CANCELLED;
}
}
// Value Object immutabile
public record Money(BigDecimal amount, Currency currency) {
public static Money sum(List<Money> values) {
return new Money(
values.stream()
.map(Money::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add),
values.get(0).currency()
);
}
}
境界のあるコンテキスト: ドメインの境界
Un 境界のあるコンテキスト そしてその中にある言語的および機能的境界 ドメイン モデルが有効であり、一貫性があること。限定されたコンテキスト内では、各用語には次のものがあります。 正確で明確な意味。コンテキストの境界を越えて、同じコンセプトが存在する可能性があります。 さまざまな意味。
具体例: の概念 "製品" コンテキストによっては異なる意味を持ちます。
- Nel カタログ: 製品には名前、説明、画像、カテゴリがあります
- Nel 倉庫: 製品には利用可能な数量、場所、再注文しきい値があります
- で」注文: 単価、注文数量、割引を含む製品と明細
- Nella 配送: 製品には重量、寸法、脆弱性があります。
単一モデルの強制 Product これらすべてのコンテキストを満たすと、
神クラス 数十の属性があり、そのほとんどは
それぞれの特定のコンテキスト。
// SBAGLIATO: Un unico modello Product per tutti i contesti
// "God Class" che viola il principio di bounded context
public class Product {
// Dati catalogo
private String name;
private String description;
private List<String> images;
private Category category;
// Dati magazzino
private int stockQuantity;
private String warehouseLocation;
private int reorderThreshold;
// Dati ordine
private BigDecimal unitPrice;
private BigDecimal discount;
// Dati spedizione
private double weight;
private Dimensions dimensions;
private boolean fragile;
// ... 30+ attributi
}
// CORRETTO: Un modello per ogni bounded context
// Catalogo
public class CatalogProduct {
private UUID id;
private String name;
private String description;
private List<String> images;
private Category category;
}
// Magazzino
public class InventoryItem {
private UUID productId; // riferimento, non l'oggetto intero
private int stockQuantity;
private String location;
private int reorderThreshold;
}
// Ordine
public class OrderLineItem {
private UUID productId; // riferimento
private Money unitPrice;
private int quantity;
private Money discount;
}
基本ルール
境界付けられた各コンテキストには、 自身のモデル 共通のコンセプトのために。 コンテキストは以下を介して通信します 参照ID物体を通してではなく 共有されました。これは、モジュールを独立させて抽出可能にする重要な原則です。
ユビキタス言語: 文脈を表す語彙
L'ユビキタス言語 開発者とドメイン専門家の間で共有される語彙 限定されたコンテキスト内で。各コンテキストには独自の言語があり、同じ用語を使用することもできます。 異なる文脈では異なる意味を持ちます。
この概念は、コード内の名前付けに実際的な影響を及ぼします。
- フォームで 注文:
Customer注文者、請求先住所、支払い方法 - フォームで 配送:
Customerおよび受取人、配送先住所と配送希望 - フォームで サポート:
Customerチケットを開いた人、インタラクション履歴と優先度レベル
単一モデルの強制 Customer すべてのコンテキストに対して結合が作成され、結合が防止されます。
チームが独自にモデルを進化させます。
コンテキスト マッピング: コンテキストの通信方法
Il コンテキストマッピング 境界のあるコンテキストがどのように相互作用するかを定義します。 さまざまな関係パターンがあり、それぞれに特定のトレードオフがあります。
破損防止層 (ACL)
L'腐敗防止層 境界のあるコンテキストを分離する翻訳レイヤー 別のコンテキストのモデルから。 Order モジュールが Catalog モジュールからのデータを必要とする場合、 カタログ DTO を直接使用するのではなく、ACL を通じてカタログ DTO を内部モデルに変換します。
// Anti-Corruption Layer nel modulo Order
// Traduce i concetti del Catalog nel linguaggio dell'Order
package com.ecommerce.order.internal.acl;
@Component
class CatalogAntiCorruptionLayer {
private final CatalogModuleApi catalogModule;
// Traduce ProductDto (catalogo) in OrderProduct (ordine)
public OrderProduct resolveProduct(UUID productId) {
ProductDto catalogProduct = catalogModule.findById(productId)
.orElseThrow(() -> new ProductNotAvailableException(productId));
// Traduzione: prende solo ciò che serve al contesto Order
return new OrderProduct(
catalogProduct.id(),
catalogProduct.name(),
Money.of(catalogProduct.price(), Currency.EUR),
catalogProduct.isAvailable()
);
}
}
// OrderProduct: modello interno del contesto Order
// Non e il ProductDto del Catalog
record OrderProduct(
UUID productId,
String displayName,
Money price,
boolean available
) {}
共有カーネル
Lo 共有カーネル および、2 つ以上の境界のある間で共有されるコードの小さなセット
コンテキスト。通常は、共通の値オブジェクト ( Money, Address)、
共有され、継続的に行われるドメイン イベント。共有カーネルは次のとおりである必要があります。 最小限の
関係するすべてのチームの同意がある場合にのみ変更されます。
// Shared Kernel: codice condiviso tra moduli
package com.ecommerce.shared;
// Value objects condivisi
public record Money(BigDecimal amount, Currency currency) {
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException();
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
public record Address(
String street, String city,
String zipCode, String country
) {}
// Eventi di dominio condivisi
public record OrderCreatedEvent(
UUID orderId, UUID userId, Money total, Instant timestamp
) {}
顧客/サプライヤー
パターンでは 顧客/サプライヤー、コンテキスト (サプライヤー) がデータまたはサービスを提供します。 別のコンテキスト (顧客) へ。サプライヤーはインターフェースを定義しますが、ニーズを考慮します 顧客の。このパターンは、あるモジュールが何らかの形で別のモジュールに明らかに依存している場合に適用されます。 一方向性。
集約設計: トランザクションと一貫性
アグリゲートの設計はモジュール式モノリスにとって非常に重要です。 国境 トランザクション的な。 ACID トランザクションは、単一のアグリゲート内で動作する必要があります。 異なる集合体間の相互作用は (同じモジュール内であっても) で管理する必要があります。 最終的な整合性 ドメインイベントを通じて。
集計の設計ルール
- 小さい: 集合体はできるだけ小さくなければなりません。一貫性が必要なものだけを単一のトランザクションに集約する
- IDによる参照: 集計には他の集計への直接参照は含まれず、その ID のみが含まれます。
- アグリゲートごとに 1 つのトランザクション: 同じトランザクション内で複数の集計を編集しないでください。
- 集計間の一貫性の可能性: ドメイン イベントを使用して、異なる集計間の変更を調整します
// Design corretto degli Aggregates
// Order Aggregate - confine transazionale
public class Order {
private UUID id;
private UUID userId; // riferimento per ID, non User object
private UUID paymentId; // riferimento per ID, non Payment object
private List<OrderItem> items; // parte dell'aggregate
private OrderStatus status;
// Operazione transazionale: avviene tutta nello stesso aggregate
@Transactional
public void addItem(UUID productId, int quantity, Money price) {
// Verifica invarianti interne
if (this.status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify confirmed order");
}
this.items.add(new OrderItem(productId, quantity, price));
this.recalculateTotal();
}
}
// SBAGLIATO: transazione che attraversa più aggregate
@Transactional
public void createOrderAndReserveStock(CreateOrderCmd cmd) {
Order order = orderRepo.save(Order.create(cmd));
// VIOLAZIONE: modifica un altro aggregate nella stessa transazione
inventoryService.reserveStock(order.getItems());
// Se reserveStock fallisce, tutto il rollback e complesso
}
// CORRETTO: eventual consistency tra aggregate
@Transactional
public void createOrder(CreateOrderCmd cmd) {
Order order = orderRepo.save(Order.create(cmd));
// Pubblica evento: il modulo Inventory reagira in modo asincrono
events.publish(new OrderCreatedEvent(order.getId(), order.getItems()));
}
境界のあるコンテキストの特定: 実践的な方法
正しい境界付きコンテキストを特定することは、科学であると同時に芸術でもあります。ここに方法があります 実際には 4 つのステップで構成されます。
- イベントストーミング: 開発者とドメインの専門家を集めます。オレンジ色のポストイットですべてのドメイン イベントを特定します。イベントを論理クラスターにグループ化します。各クラスターは、潜在的に限定されたコンテキストです。
- 言語分析: 同じ用語が異なる意味を持っている場所を識別します。それぞれの言語の相違は文脈の境界を示します
- 依存関係の分析: クラスター間の依存関係をマップします。外部依存関係がほとんどないクラスターは、独立したモジュールの良い候補です
- チームで検証: 特定された境界がチームの組織構造とスキルを反映していることを確認します。
コンウェイの法則
La コンウェイの法則 組織はミラーリングするシステムを設計すると述べています コミュニケーションの構造。実際には: モジュールの境界は一致している必要があります。 チームの境界線がある。 Order モジュールを担当するチームは、次のことを行う必要はありません。 作業を完了するために毎日出荷モジュール チームと調整します。
コードの構造がチームの構造を反映していない場合、マージ競合という摩擦が生じます。 頻繁かつ継続的な調整会議、相互ブロック。アーキテクチャを調整し、 これはモジュール式モノリスの組織化と成功の前提条件です。
次の記事
次回の記事では、次のトピックについて取り上げます。 データベース設計 モジュラーモノリスの場合: 共有スキーマとモジュールごとのスキーマ、データ所有権、分散トランザクション管理、 そして最終的な整合性を実現するための Saga や Event Sourcing などのパターン。境界のあるコンテキストを翻訳します ここでは具体的なデータ構造で識別されます。







