両派を行き来する ─ Aggregate と Partition Key、Repository の壁
第 1 章で「ドメイン駆動と特性駆動は別レイヤーの話だ」と書いた。第 2 章から第 17 章まで、各ワークロードと共通基盤を歩いてきた。ここで両派が実際に交わる場所を 5 つ挙げ、行き来するための作法を統合する。
交点 1:Aggregate と Partition Key
最も顕著な交点。
| ドメイン視点 | 特性視点 |
|---|---|
| Aggregate = 整合性境界 | Partition = 物理的な配置単位 |
| Trans 境界 = 1 Aggregate | Trans 境界 = Partition 内 |
| ID 参照で疎結合 | Partition 横断はクエリコスト |
幸運な対応:DynamoDB のような NoSQL では、Aggregate ≒ 同一 Partition Key のデータ集合として綺麗に重ねられる。
// Aggregate Root の永続化
class Order {
id: OrderId;
items: OrderItem[];
shipping: Shipping;
}
// → DynamoDB
{ PK: `ORDER#${id}`, SK: 'METADATA', ... }
{ PK: `ORDER#${id}`, SK: 'ITEM#1', ... }
{ PK: `ORDER#${id}`, SK: 'ITEM#2', ... }
{ PK: `ORDER#${id}`, SK: 'SHIPPING', ... }
// → 全 item が同 partition、1 query で取得、TransactWriteItems で atomic
不幸な対応:複数 Aggregate にまたがるクエリが必要なとき。例:「全 user の注文を最新順に」。これは Order Aggregate の境界を超える。
選択肢:
- GSI で別 axis のクエリを許す(NoSQL)
- Read Model を別 DB に作る(OLAP / Search)
- データ重複を受け入れて denormalize(NoSQL 的)
判断軸: 頻度 × ビジネスインパクトで選ぶ。低頻度なら 1、高頻度で集計型なら 2、リアルタイム性が高いなら 3。
交点 2:Trans 境界 と Aggregate
第 5 章で扱ったが、ここで一段抽象化する:
graph TB
subgraph "理想(DDD の原則)"
A1[1 Aggregate = 1 Trans]
end
subgraph "現実"
R1[1 Trans に複数 Aggregate を入れたい場面]
R2[1 Aggregate が複数 Trans に分割される場面]
end
A1 -.侵食する.-> R1
A1 -.侵食する.-> R2
style A1 fill:#e1f5ff
style R1 fill:#ffe1e1
style R2 fill:#ffe1e1
“1 Trans に複数 Aggregate” が起きる理由
ユースケースが 2 つの Aggregate を同時に変える 業務要件を持つ。例:「ユーザー登録と同時に最初の注文を作る」。
選択肢:
- 例外として 1 Trans に入れる(Postgres なら可能)
- Saga で補償可能な連鎖にする(DB 跨ぎなら必須)
- Aggregate の境界を見直す(実は 1 つの Aggregate だった可能性)
“1 Aggregate が複数 Trans に分割” が起きる理由
DynamoDB の TransactWriteItems の 25 件制限、巨大な Aggregate(巨大コレクションを持つ)。
選択肢:
- Aggregate を分割する(Vernon 推奨:Aggregate を小さく)
- 段階的更新(Saga 内部化):状態機械で部分的な更新を許す
- イベント源にする:状態を直接更新せず、event を append(Event Sourcing)
交点 3:Repository の限界
Repository パターンは Aggregate の永続化 に有効だが、全クエリには対応しない。
Repository でできること
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
remove(id: OrderId): Promise<void>;
}
「ID で取得 / 保存 / 削除」── Aggregate の単純な CRUD。
Repository で苦しいこと
- 複雑な検索:「2026 年 5 月の active な注文を売上順に 100 件」
- 集計:「商品カテゴリ別月次売上」
- JOIN を要するクエリ
これらは Aggregate を単位とするインターフェースに無理に押し込めると 不自然。Vernon も実践 DDD で Query Service や Read Model を勧めている。
// 書き込み: Repository
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: OrderId): Promise<Order | null>;
}
// 読み出し: Query Service(Aggregate を返さなくてよい)
interface OrderQueryService {
search(criteria: OrderSearchCriteria): Promise<OrderListItem[]>;
monthlySalesByCategory(month: Month): Promise<CategorySales[]>;
}
// 別 DB の Read Model から自由にクエリ
class OrderQueryServiceImpl implements OrderQueryService {
constructor(
private readonly clickhouse: ClickHouseClient,
private readonly elasticsearch: ESClient,
) {}
// ...
}
Repository(書き込み)と Query Service(読み出し)の分離。これは CQRS の現実的な実装。
交点 4:共通基盤の言語
第 16 章で扱った「Repository が DAO に堕ちる」問題を、両派の交点として再整理する。
ドメイン側の語彙
// 注文ドメイン
class Order { /* ... */ }
class Customer { /* ... */ }
class OrderRepository { save(o: Order) }
共通基盤側の語彙
// 認可基盤
class Permission { /* ... */ } // ← 注文ドメインは知らない
interface AuthorizationService {
hasPermission(...): Promise<boolean>;
}
共通基盤の Permission は誰のドメインオブジェクトか?
- 共通基盤チームのドメイン:認可ロジックを売る/提供する立場
- 使う側から見れば:単なる API、技術的な詳細
これは ドメイン境界の入れ子構造。共通基盤は自分自身のドメインを持つが、それは利用ドメインの インフラ として現れる。
graph TB
subgraph "利用ドメイン側の世界"
UO[Order Aggregate]
UR[Order Repository]
end
subgraph "共通基盤側の世界"
P[Permission]
AS[Authorization Service]
end
UO -. 認可チェック .-> AS
UO -. ドメインオブジェクト .-> UR
Note1[同じ AS が、利用側からは "詳細"<br/>共通基盤側からは "ドメイン"]
style UO fill:#e1f5ff
style P fill:#ffe1e1
両派は 立場で見え方が違う。これを認識すると、「Repository が DAO に堕ちた」という違和感は解消する。共通基盤側からは Permission が Aggregate になりうる。
交点 5:Read Model の所有権
第 17 章で扱った CQRS の Read Model は、誰のものか。
パターン A:Read Model は Source ドメインの所有
Order ドメイン:
- Order Aggregate(Postgres)
- OrderListView(ClickHouse) ← 同じドメインが所有
- OrderSearchView(ES) ← 同じドメインが所有
projection は同一ドメイン内で管理。利点:ドメインの一貫性。
パターン B:Read Model は別ドメイン(分析ドメイン)の所有
Order ドメイン:
- Order Aggregate(Postgres)
Analytics ドメイン:
- SalesFact(ClickHouse) ← 別ドメインが所有
- CustomerInsight(ClickHouse)
projection は分析ドメインに移管される。利点:分析ドメインが独立して進化できる(yuzutas0 が言うアジャイルデータモデリング)。
判断軸
| 場面 | 推奨 |
|---|---|
| Read Model が Source ドメインのクエリ要件のみ | パターン A |
| 複数ドメインが集まる分析が中心 | パターン B |
| データ基盤がチームとして独立 | パターン B |
| マイクロサービスで独立 | パターン A 寄り |
両者は対立ではなく、組織と業務の分割線で決まる。
行き来のための設計判断フレーム
ここまでの 5 交点を踏まえ、両派を行き来する設計判断のフレームを提示する:
graph TB
Start[新規システム設計の起点]
Start --> D1[ドメイン視点:<br/>Bounded Context を引く]
Start --> W1[特性視点:<br/>主要ワークロードを 6 軸で見る]
D1 --> D2[Aggregate を試案]
W1 --> W2[主 DB を選定]
D2 --> Match{Aggregate と Partition が<br/>整合する?}
W2 --> Match
Match -->|Yes| OK[統合的に設計]
Match -->|No| Adjust[Aggregate 再設計 or<br/>DB 選定見直し]
Adjust --> D2
Adjust --> W2
OK --> Polyglot{他のワークロードがある?}
Polyglot -->|Yes| AddDB[追加 DB を選定<br/>Source of Truth を決める]
Polyglot -->|No| Done[完成]
AddDB --> Sync[同期戦略を決める<br/>Outbox or CDC]
Sync --> Done
style Start fill:#e1f5ff
style OK fill:#e1ffe1
style Done fill:#e1ffe1
行き来する作法 ── 5 つの原則
- Bounded Context が起点。共通基盤は別ドメイン(別 Bounded Context)として扱う
- Aggregate と Partition の整合を最初にチェック。整合しないなら Aggregate を再考
- Source of Truth を 1 つに決める。複数の “正” を許さない
- Repository(書き込み)と Query Service(読み出し)を分離。CQRS の現実的実装
- Polyglot は最後に。シンプルから始めて、必要になったら分割
この章の要点
- 両派が交わる 5 つの場所: Aggregate↔Partition / Trans 境界 / Repository / 共通基盤 / Read Model
- Aggregate ≒ Partition Key で対応するのが NoSQL の幸運
- Trans 境界が DB 特性で変わる ── Saga / Aggregate 再設計 / Event Sourcing
- Repository は書き込み用、Query Service は読み出し用(CQRS)
- 共通基盤の語彙は使い手から見れば詳細、共通基盤側から見ればドメイン
- 行き来の 5 原則: Bounded Context 起点 / Aggregate ↔ Partition / Source of Truth / R/W 分離 / Polyglot は最後
次章への問いかけ
19 章までで、ドメイン駆動と特性駆動の地図、6 ワークロード、共通基盤、Polyglot を見てきた。最後に、これらを 設計の判断軸 としてチェックリスト化する。
最終章で 外さない 9 つの軸。