目次を表示する

DB 設計の軸 2026 ─ ドメイン駆動と特性駆動の二つの流派を行き来する 19 章

両派を行き来する ─ Aggregate と Partition Key、Repository の壁

両派を行き来する ─ Aggregate と Partition Key、Repository の壁

第 1 章で「ドメイン駆動と特性駆動は別レイヤーの話だ」と書いた。第 2 章から第 17 章まで、各ワークロードと共通基盤を歩いてきた。ここで両派が実際に交わる場所を 5 つ挙げ、行き来するための作法を統合する。

交点 1:Aggregate と Partition Key

最も顕著な交点。

ドメイン視点特性視点
Aggregate = 整合性境界Partition = 物理的な配置単位
Trans 境界 = 1 AggregateTrans 境界 = 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 の境界を超える。

選択肢:

  1. GSI で別 axis のクエリを許す(NoSQL)
  2. Read Model を別 DB に作る(OLAP / Search)
  3. データ重複を受け入れて 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. 例外として 1 Trans に入れる(Postgres なら可能)
  2. Saga で補償可能な連鎖にする(DB 跨ぎなら必須)
  3. Aggregate の境界を見直す(実は 1 つの Aggregate だった可能性)

“1 Aggregate が複数 Trans に分割” が起きる理由

DynamoDB の TransactWriteItems25 件制限、巨大な Aggregate(巨大コレクションを持つ)。

選択肢:

  1. Aggregate を分割する(Vernon 推奨:Aggregate を小さく)
  2. 段階的更新(Saga 内部化):状態機械で部分的な更新を許す
  3. イベント源にする:状態を直接更新せず、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 ServiceRead 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 つの原則

  1. Bounded Context が起点。共通基盤は別ドメイン(別 Bounded Context)として扱う
  2. Aggregate と Partition の整合を最初にチェック。整合しないなら Aggregate を再考
  3. Source of Truth を 1 つに決める。複数の “正” を許さない
  4. Repository(書き込み)と Query Service(読み出し)を分離。CQRS の現実的実装
  5. 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 つの軸