TypeScript・Pythonで実装するDDD戦術的パターン実践ガイド
DDD(ドメイン駆動設計)の戦術的パターンは、ビジネスロジックをコードに正確に反映するための設計手法です。この記事では、集約・値オブジェクト・リポジトリの3つの中核パターンを、TypeScriptとPythonの両方で実装しながら解説します。
この記事でわかること
- 値オブジェクト・エンティティ・集約の違いと、それぞれの実装方法
- Vaughn Vernonが提唱する集約設計の4つのルールと、違反時に起こる問題
- TypeScript(Object.freeze + ファクトリメソッド)とPython(frozen dataclass)での値オブジェクト実装
- リポジトリパターンによるドメイン層と永続化層の分離手法
- 集約間の結果整合性をドメインイベントで実現する設計
対象読者
- 想定読者: DDDに興味があるが実装経験の少ないソフトウェアエンジニア
-
必要な前提知識:
- TypeScript または Python の基礎文法
- オブジェクト指向プログラミングの基本概念(クラス、インターフェース、継承)
- RDB の基本操作(SELECT / INSERT / UPDATE)
MLエンジニアの方へ: DDDはML基盤のAPI設計やデータパイプラインにも応用できます。Pythonの
@dataclass(frozen=True)はMLで馴染みのある記法なので、そこからDDDの概念に入りやすいでしょう。
結論・成果
DDDの戦術的パターンを適用すると、以下の効果が報告されています。
- Vaughn Vernonの調査によると、集約の約70%はルートエンティティと値オブジェクトのみで構成でき、残り30%も2〜3エンティティに収まる(Effective Aggregate Design Part I)
- MicrosoftのAzure Architecture Centerでは、マイクロサービス設計において「1トランザクション=1集約」の原則がスケーラビリティ向上に寄与すると報告されている
- 値オブジェクトの導入により、バリデーションロジックの重複を排除し、バグの発生箇所を限定できる
値オブジェクトを実装する
値オブジェクトは、DDDの戦術的パターンの中で最も導入しやすいパターンです。Pythonの int や str のように「値そのもの」に意味があり、同じ値なら同一と見なすオブジェクトです。MLエンジニアにとっては、NumPyの配列比較(np.array_equal)に近い考え方です。
値オブジェクトの3つの特性
値オブジェクトには以下の特性があります。
| 特性 | 説明 | 例 |
|---|---|---|
| 不変性(Immutability) | 一度作成したら変更できない |
Money(100, "JPY") は金額を後から変えられない |
| 構造的等価性(Structural Equality) | 全属性が同じなら同一と見なす |
Money(100, "JPY") == Money(100, "JPY") は true
|
| 自己検証(Self-Validation) | 不正な値では生成できない |
Money(-1, "JPY") はエラーになる |
MLで例えると、torch.tensor([1, 2, 3]) と torch.tensor([1, 2, 3]) の値が等しいのと同じ発想です。IDで区別する「エンティティ」との違いは後述します。
TypeScriptでの値オブジェクト実装
TypeScriptでは Object.freeze とプライベートコンストラクタを組み合わせて不変性を実現します。
// value-object.base.ts
// 値オブジェクトの基底クラス
interface ValueObjectProps {
[index: string]: unknown;
}
export abstract class ValueObject<T extends ValueObjectProps> {
public readonly props: T;
protected constructor(props: T) {
// Object.freeze で不変性を保証
this.props = Object.freeze(props);
}
// 構造的等価性: 全プロパティが一致すれば同一
public equals(vo?: ValueObject<T>): boolean {
if (vo === null || vo === undefined) return false;
return JSON.stringify(this.props) === JSON.stringify(vo.props);
}
}
具体的な値オブジェクト Money を実装してみましょう。
// money.ts
// 金額を表す値オブジェクト
interface MoneyProps {
amount: number;
currency: string;
}
export class Money extends ValueObject<MoneyProps> {
// ファクトリメソッドでバリデーション付き生成
static create(amount: number, currency: string): Money {
if (amount < 0) {
throw new Error("金額は0以上である必要があります");
}
if (!["JPY", "USD", "EUR"].includes(currency)) {
throw new Error(`未対応の通貨: ${currency}`);
}
return new Money({ amount, currency });
}
get amount(): number {
return this.props.amount;
}
get currency(): string {
return this.props.currency;
}
// 値オブジェクトは新しいインスタンスを返す(不変性を維持)
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("異なる通貨同士の加算はできません");
}
return Money.create(this.amount + other.amount, this.currency);
}
}
なぜファクトリメソッド(static create)を使うのか:
- コンストラクタを
protectedにすることで、バリデーションなしの生成を防止する -
new Money(...)を直接呼べないため、不正な状態のオブジェクトが存在できない - Pythonの
__init__でバリデーションするのと同じ意図だが、TypeScriptでは慣習的にファクトリメソッドを使う
Pythonでの値オブジェクト実装
Pythonでは @dataclass(frozen=True) を使います。MLエンジニアには馴染みのある dataclass に frozen=True を付けるだけで不変性が得られます。
# value_objects.py
# Python 3.10+ を想定
from dataclasses import dataclass
@dataclass(frozen=True, slots=True) # slots=True でメモリ効率改善
class Money:
"""金額を表す値オブジェクト"""
amount: int
currency: str
def __post_init__(self) -> None:
# frozen=True でも __post_init__ 内では object.__setattr__ で検証可能
if self.amount < 0:
raise ValueError("金額は0以上である必要があります")
if self.currency not in ("JPY", "USD", "EUR"):
raise ValueError(f"未対応の通貨: {self.currency}")
def add(self, other: "Money") -> "Money":
"""新しいインスタンスを返す(不変性を維持)"""
if self.currency != other.currency:
raise ValueError("異なる通貨同士の加算はできません")
return Money(amount=self.amount + other.amount, currency=self.currency)
# 使用例
price = Money(amount=1000, currency="JPY")
tax = Money(amount=100, currency="JPY")
total = price.add(tax) # Money(amount=1100, currency="JPY")
# 構造的等価性(dataclass が自動生成)
assert Money(100, "JPY") == Money(100, "JPY") # True
# 不変性(frozen=True により代入エラー)
# price.amount = 2000 # FrozenInstanceError
注意:
frozen=Trueの dataclass では、__post_init__内でself.amount = ...のような代入はできません。バリデーションのみ行い、値の変換が必要な場合はobject.__setattr__(self, 'amount', new_value)を使います。ただし、この手法は不変性の保証を弱めるため、ファクトリメソッド(@classmethod)での生成を推奨します。
よくある間違い: プリミティブ型をそのまま使う
値オブジェクトを使わずにプリミティブ型(string, number)をそのまま使うアンチパターンはプリミティブ執着(Primitive Obsession)と呼ばれます。
// ❌ アンチパターン: プリミティブ執着
function createOrder(
userId: string, // ← どんな文字列でも渡せてしまう
productId: string, // ← userId と取り違えても型エラーにならない
price: number, // ← 通貨が不明、負の値も渡せる
email: string // ← メールアドレスのフォーマット検証がない
): void { /* ... */ }
// ✅ 値オブジェクトを使う
function createOrder(
userId: UserId, // ← UserId 型でしか渡せない
productId: ProductId, // ← ProductId 型なので取り違えない
price: Money, // ← 通貨付き、負の値は生成時に弾かれる
email: EmailAddress // ← フォーマット検証済み
): void { /* ... */ }
制約: 値オブジェクトを多用するとクラス数が増え、シリアライゼーション(JSON変換)の実装が必要になります。ORMとの統合にも追加の設定が必要です。小規模プロジェクト(エンティティ10個以下)ではオーバーヘッドが利点を上回る場合があります。
エンティティと集約を設計する
エンティティは値オブジェクトとは異なり、一意のIDで識別されるオブジェクトです。MLで例えると、学習済みモデルの各バージョン(model-v1, model-v2)のように、内容が変わってもIDで同一性を追跡するものです。
エンティティと値オブジェクトの比較
| 比較項目 | 値オブジェクト | エンティティ |
|---|---|---|
| 同一性の判定 | 全属性の一致 | IDの一致 |
| 可変性 | 不変 | 可変(IDは不変) |
| ライフサイクル | なし(使い捨て) | あり(生成→更新→削除) |
| 例 | Money, Email, Address | User, Order, Product |
集約とは何か
集約(Aggregate)は、エンティティと値オブジェクトをひとまとめにした「整合性の境界」です。集約の外部からは集約ルート(Aggregate Root)だけにアクセスし、内部のオブジェクトには直接触れません。
MLで例えると、MLパイプラインの「実験」(Experiment)が集約ルート、各「実行」(Run)が子エンティティ、ハイパーパラメータが値オブジェクトに相当します。外部からは実験ID経由でしかアクセスしないのと同じ構造です。
Vernon の集約設計4ルール
Vaughn Vernonは著書『実践ドメイン駆動設計』(通称「赤本」)で、以下の4つのルールを提唱しています(ArchiLab - Aggregate Design Rules)。
| # | ルール | 説明 | 違反時の問題 |
|---|---|---|---|
| 1 | 真の不変条件を整合性境界でモデル化する | 1つのトランザクションで必ず一貫性を保つべきデータだけを集約にまとめる | 不要なデータまで含めて集約が肥大化する |
| 2 | 小さい集約を設計する | ルートエンティティ + 最小限の値オブジェクト/子エンティティ | 集約が大きいとロック競合が発生し、スケーラビリティが低下する |
| 3 | 他の集約はIDで参照する | 直接のオブジェクト参照ではなく、IDで間接参照する | 他の集約を変更する誘惑が生まれ、トランザクション境界を侵す |
| 4 | 境界外では結果整合性を使う | 集約間の整合性はドメインイベントで非同期に保つ | 複数集約を1トランザクションで更新しようとして、デッドロックやパフォーマンス低下が起こる |
Vernonの調査によると、約70%の集約はルートエンティティと値オブジェクトのみで構成でき、残り30%も2〜3エンティティに収まると報告されています(Effective Aggregate Design Part I)。
TypeScriptでの集約実装
ECサイトの注文(Order)を集約として実装してみましょう。
// order.ts
// 注文集約
// --- 値オブジェクト ---
class OrderId extends ValueObject<{ value: string }> {
static create(value: string): OrderId {
if (!value || value.length === 0) {
throw new Error("OrderIdは空にできません");
}
return new OrderId({ value });
}
get value(): string {
return this.props.value;
}
}
class ProductId extends ValueObject<{ value: string }> {
static create(value: string): ProductId {
return new ProductId({ value });
}
get value(): string {
return this.props.value;
}
}
// --- 子エンティティ ---
class OrderLine {
constructor(
public readonly productId: ProductId,
public readonly quantity: number,
public readonly unitPrice: Money,
) {
if (quantity <= 0) {
throw new Error("数量は1以上である必要があります");
}
}
get subtotal(): Money {
return Money.create(
this.unitPrice.amount * this.quantity,
this.unitPrice.currency,
);
}
}
// --- ドメインイベント ---
interface DomainEvent {
occurredOn: Date;
}
class OrderPlacedEvent implements DomainEvent {
constructor(
public readonly orderId: string,
public readonly totalAmount: number,
public readonly occurredOn: Date = new Date(),
) {}
}
// --- 集約ルート ---
class Order {
private lines: OrderLine[] = [];
private domainEvents: DomainEvent[] = [];
private constructor(
public readonly id: OrderId,
private status: "draft" | "placed" | "cancelled",
) {}
static create(id: OrderId): Order {
return new Order(id, "draft");
}
// 集約ルート経由でのみ明細を操作(外部から直接 lines にアクセスさせない)
addLine(productId: ProductId, quantity: number, unitPrice: Money): void {
if (this.status !== "draft") {
throw new Error("確定済みの注文には明細を追加できません");
}
// ビジネスルール: 1注文あたり最大20明細
if (this.lines.length >= 20) {
throw new Error("1注文あたりの明細数上限(20)を超えています");
}
this.lines.push(new OrderLine(productId, quantity, unitPrice));
}
place(): void {
if (this.lines.length === 0) {
throw new Error("明細が空の注文は確定できません");
}
this.status = "placed";
// ドメインイベントを記録(後で発行)
this.domainEvents.push(
new OrderPlacedEvent(this.id.value, this.totalAmount.amount),
);
}
get totalAmount(): Money {
if (this.lines.length === 0) {
return Money.create(0, "JPY");
}
return this.lines.reduce(
(sum, line) => sum.add(line.subtotal),
Money.create(0, this.lines[0].unitPrice.currency),
);
}
// ドメインイベントの取得と消費
pullDomainEvents(): DomainEvent[] {
const events = [...this.domainEvents];
this.domainEvents = [];
return events;
}
}
なぜ addLine を集約ルート経由にするのか:
- 明細数の上限チェック(20件まで)は注文全体を見ないと判定できない
- ステータスが
draftのときだけ操作可能というビジネスルールを強制できる - 外部から
order.lines.push(...)を直接呼ばせると、これらの不変条件が守れない
Pythonでの集約実装
# order.py
# 注文集約の Python 実装
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol
@dataclass(frozen=True, slots=True)
class OrderId:
value: str
def __post_init__(self) -> None:
if not self.value:
raise ValueError("OrderIdは空にできません")
@dataclass(frozen=True, slots=True)
class ProductId:
value: str
@dataclass(frozen=True, slots=True)
class OrderLine:
product_id: ProductId
quantity: int
unit_price: Money
def __post_init__(self) -> None:
if self.quantity <= 0:
raise ValueError("数量は1以上である必要があります")
@property
def subtotal(self) -> Money:
return Money(
amount=self.unit_price.amount * self.quantity,
currency=self.unit_price.currency,
)
# ドメインイベント
@dataclass(frozen=True)
class OrderPlacedEvent:
order_id: str
total_amount: int
occurred_on: datetime = field(default_factory=datetime.now)
# 集約ルート(エンティティなので frozen=False)
class Order:
def __init__(self, order_id: OrderId) -> None:
self.id = order_id
self._lines: list[OrderLine] = []
self._status: str = "draft"
self._domain_events: list[object] = []
def add_line(
self, product_id: ProductId, quantity: int, unit_price: Money
) -> None:
if self._status != "draft":
raise ValueError("確定済みの注文には明細を追加できません")
if len(self._lines) >= 20:
raise ValueError("1注文あたりの明細数上限(20)を超えています")
self._lines.append(OrderLine(product_id, quantity, unit_price))
def place(self) -> None:
if not self._lines:
raise ValueError("明細が空の注文は確定できません")
self._status = "placed"
self._domain_events.append(
OrderPlacedEvent(
order_id=self.id.value,
total_amount=self.total_amount.amount,
)
)
@property
def total_amount(self) -> Money:
if not self._lines:
return Money(amount=0, currency="JPY")
total = 0
currency = self._lines[0].unit_price.currency
for line in self._lines:
total += line.subtotal.amount
return Money(amount=total, currency=currency)
def pull_domain_events(self) -> list[object]:
events = list(self._domain_events)
self._domain_events.clear()
return events
# エンティティの等価性: IDで判定
def __eq__(self, other: object) -> bool:
if not isinstance(other, Order):
return False
return self.id == other.id
def __hash__(self) -> int:
return hash(self.id)
ハマりポイント: 集約ルート(
Order)は状態が変わるためfrozen=Trueにできません。一方、子エンティティ(OrderLine)や値オブジェクト(Money)は不変にしましょう。「何を可変にするか」の判断基準は「ライフサイクルの中で状態遷移があるか」です。
リポジトリパターンで永続化を分離する
リポジトリパターンは、ドメイン層が永続化の詳細(どのDBを使うか、SQLの書き方など)を知らなくて済むようにするパターンです。MLエンジニアにとっては、MLflowの MlflowClient がモデルの保存先(ローカル/S3/GCS)を抽象化するのと同じ発想です。
リポジトリの基本原則
リポジトリの基本ルールは以下の3つです。
-
集約ルートごとに1つ:
OrderRepositoryはOrder集約全体を保存/取得する。OrderLine単独のリポジトリは作らない - インターフェースはドメイン層に定義: 実装はインフラ層に置き、依存関係を逆転させる(DIP: 依存関係逆転の原則)
-
ドメインオブジェクトを返す: DBのレコードではなく、集約(
Order)を返す
TypeScriptでのリポジトリ実装
// order-repository.ts
// リポジトリのインターフェース(ドメイン層に定義)
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
nextId(): OrderId;
}
// prisma-order-repository.ts
// Prisma ORM を使ったリポジトリ実装(インフラ層)
import { PrismaClient } from "@prisma/client";
export class PrismaOrderRepository implements OrderRepository {
constructor(private readonly prisma: PrismaClient) {}
async findById(id: OrderId): Promise<Order | null> {
// 集約全体を1クエリで取得(N+1問題を回避)
const record = await this.prisma.order.findUnique({
where: { id: id.value },
include: { lines: true }, // 子エンティティも一緒に取得
});
if (!record) return null;
return this.toDomain(record);
}
async save(order: Order): Promise<void> {
const data = this.toPersistence(order);
// Upsert で冪等性を確保
await this.prisma.order.upsert({
where: { id: order.id.value },
create: data,
update: data,
});
}
nextId(): OrderId {
// UUIDv4 で一意なIDを生成
return OrderId.create(crypto.randomUUID());
}
// --- マッパー(ドメイン ↔ 永続化の変換) ---
private toDomain(record: OrderRecord): Order {
// DB レコードからドメインオブジェクトを再構築
const order = Order.reconstruct(
OrderId.create(record.id),
record.status,
record.lines.map(
(l) =>
new OrderLine(
ProductId.create(l.productId),
l.quantity,
Money.create(l.unitPrice, l.currency),
),
),
);
return order;
}
private toPersistence(order: Order): OrderCreateInput {
return {
id: order.id.value,
status: order.status,
totalAmount: order.totalAmount.amount,
currency: order.totalAmount.currency,
lines: {
// 明細は洗い替え(delete → create)で簡潔に
deleteMany: {},
create: order.getLines().map((l) => ({
productId: l.productId.value,
quantity: l.quantity,
unitPrice: l.unitPrice.amount,
currency: l.unitPrice.currency,
})),
},
};
}
}
なぜ upsert を使うのか:
- 冪等性の確保: 同じ
saveを2回呼んでもエラーにならない - リトライ時の安全性: ネットワーク障害で再送されても、データの不整合が起きない
- 外部I/Oに対して冪等性を設計に含めるのは、分散システムの基本原則
Pythonでのリポジトリ実装
# order_repository.py
# リポジトリインターフェース(ドメイン層)
from abc import ABC, abstractmethod
class OrderRepository(ABC):
"""注文集約のリポジトリインターフェース"""
@abstractmethod
def find_by_id(self, order_id: OrderId) -> Order | None:
...
@abstractmethod
def save(self, order: Order) -> None:
...
@abstractmethod
def next_id(self) -> OrderId:
...
# sqlalchemy_order_repository.py
# SQLAlchemy を使ったリポジトリ実装(インフラ層)
import uuid
from sqlalchemy.orm import Session
class SqlAlchemyOrderRepository(OrderRepository):
def __init__(self, session: Session) -> None:
self._session = session
def find_by_id(self, order_id: OrderId) -> Order | None:
# joinedload で N+1 問題を回避
record = (
self._session.query(OrderModel)
.options(joinedload(OrderModel.lines))
.filter(OrderModel.id == order_id.value)
.one_or_none()
)
if record is None:
return None
return self._to_domain(record)
def save(self, order: Order) -> None:
record = self._to_persistence(order)
self._session.merge(record) # merge で upsert 相当
self._session.flush()
def next_id(self) -> OrderId:
return OrderId(value=str(uuid.uuid4()))
def _to_domain(self, record: OrderModel) -> Order:
order = Order(order_id=OrderId(value=record.id))
for line in record.lines:
order.add_line(
product_id=ProductId(value=line.product_id),
quantity=line.quantity,
unit_price=Money(amount=line.unit_price, currency=line.currency),
)
return order
def _to_persistence(self, order: Order) -> OrderModel:
return OrderModel(
id=order.id.value,
status=order._status,
lines=[
OrderLineModel(
product_id=line.product_id.value,
quantity=line.quantity,
unit_price=line.unit_price.amount,
currency=line.unit_price.currency,
)
for line in order._lines
],
)
よくある問題と解決方法
| 問題 | 原因 | 解決方法 |
|---|---|---|
| N+1 クエリ問題 | 集約の子エンティティを遅延ロードしている |
include(Prisma)/ joinedload(SQLAlchemy)で一括取得 |
| 集約の再構築が複雑 | コンストラクタがバリデーション付きのため、DBからの復元時にエラーになる |
reconstruct 用のファクトリメソッドを別途用意する |
| トランザクション境界が不明確 | リポジトリ内でトランザクションを管理してしまう | ユースケース層で begin/commit を管理し、リポジトリは save のみ担当 |
| テストが書きにくい | リポジトリがDBに依存している | インメモリ実装(Map / dict ベース)をテスト用に用意する |
制約: リポジトリパターンは、ORMが提供するクエリ機能(JOINの最適化、バッチ処理など)を直接使えなくなるトレードオフがあります。読み取り専用の複雑なクエリには、CQRS(Command Query Responsibility Segregation)を組み合わせて、リポジトリは書き込み側のみに使う方法が実用的です。
集約間の結果整合性をドメインイベントで実現する
Vernonの4つ目のルール「境界外では結果整合性を使う」を実装するには、ドメインイベントが鍵になります。注文が確定したら在庫を減らす、という例で見てみましょう。
// place-order-use-case.ts
// ユースケース層(アプリケーションサービス)
export class PlaceOrderUseCase {
constructor(
private readonly orderRepo: OrderRepository,
private readonly eventBus: EventBus,
) {}
async execute(orderId: string): Promise<void> {
// 1. 集約を取得
const order = await this.orderRepo.findById(OrderId.create(orderId));
if (!order) throw new Error("注文が見つかりません");
// 2. ビジネスロジックを実行
order.place();
// 3. 集約を保存(1トランザクション = 1集約)
await this.orderRepo.save(order);
// 4. ドメインイベントを発行(結果整合性)
const events = order.pullDomainEvents();
await this.eventBus.publishAll(events);
}
}
最初は order.place() の中で直接在庫を減らそうと考えるかもしれませんが、これはVernonのルール1・3に違反します。 注文集約が在庫集約のオブジェクト参照を持つと、2つの集約を1トランザクションで更新する誘惑が生まれ、ロック競合やスケーラビリティ低下の原因になります。
トレードオフ: 結果整合性を採用すると、注文確定と在庫減少の間にタイムラグが生じます。この間に別の注文で在庫が0になる可能性があります。これを許容するか、補償トランザクション(在庫不足時に注文をキャンセル)を設計するかは、ビジネス要件によります。
2026年の新しいアプローチ: Dynamic Consistency Boundaries
従来の集約は「クラス単位で固定された境界」でしたが、Sara Pellegriniが提唱するDynamic Consistency Boundaries(DCB)では、操作ごとに動的に境界を定義します(EventSourcingDB - DDD Back to Basics)。
DCBは「この操作で整合性が必要なデータは何か」をイベントストリームから動的に判断するアプローチで、集約クラスに縛られない柔軟性があります。ただし、まだ新しい概念であり、実績のあるプロダクション事例は限定的です。
まとめと次のステップ
まとめ:
-
値オブジェクト: 不変・構造的等価性・自己検証の3特性を持つ。TypeScriptでは
Object.freeze+ ファクトリメソッド、Pythonでは@dataclass(frozen=True)で実装する - 集約: Vernonの4ルール(真の不変条件、小さく、IDで参照、結果整合性)に従って設計する。集約の70%はルートエンティティ + 値オブジェクトだけで構成できる
- リポジトリ: 集約ルートごとに1つ作成し、ドメイン層にインターフェース、インフラ層に実装を置く。N+1問題とトランザクション境界に注意する
- ドメインイベント: 集約間の結果整合性を実現する。1トランザクション = 1集約の原則を守り、集約間の連携はイベント経由で行う
次にやるべきこと:
- 自分のプロジェクトで最も複雑なビジネスルールを持つエンティティを1つ選び、集約として再設計してみる
- 値オブジェクトの導入は小さく始める: まず
EmailAddressやUserIdのようなID系から - リポジトリのテストは、インメモリ実装(
Map/dict)を使って高速に回す
参考
- Effective Aggregate Design Part I - Vaughn Vernon
- Aggregate Design Rules (Vernon's Red Book) - ArchiLab
- Use Tactical DDD to Design Microservices - Azure Architecture Center
- Value Objects - DDD w/ TypeScript - Khalil Stemmler
- How to Design & Persist Aggregates - Khalil Stemmler
- Implementing DTOs, Mappers & the Repository Pattern - Khalil Stemmler
- Architecture Patterns with Python (Cosmic Python) - Chapter 1: Domain Model
- Martin Fowler - Value Object
- Martin Fowler - DDD Aggregate
- DDD: Back to Basics - EventSourcingDB (2026)
- Repository Pattern in 2026: Still Relevant or an Anti-Pattern?
注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。