目次を表示する

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

Polyglot Persistence ─ 複数 DB を持つときのドメイン側の構え

Polyglot Persistence ─ 複数 DB を持つときのドメイン側の構え

ここまでの章で見てきた事実:1 つの DB で全ワークロードを賄うのは現代では困難

  • OLTP は PostgreSQL
  • 分析は ClickHouse
  • Cache は Redis
  • イベント連携は Kafka
  • 検索は Elasticsearch

これが現代の典型的なスタック。複数 DB を併用する前提でドメイン側がどう構えるか ── これが本章の主題。

Polyglot Persistence の定義

Martin Fowler “Polyglot Persistence” (2011)

Different parts of the system might use different data stores, chosen for the way that data is used.

データの使われ方に応じて、システムの異なる部分で異なるデータストアを使う」。

Fowler はこれを Bounded Context と関連付ける。1 つの Bounded Context = 1 つの主 DB が原則で、Bounded Context が異なれば別 DB を選んでよい

graph TB
  subgraph "Bounded Context: 注文"
    OLTP1[PostgreSQL: 注文 OLTP]
    Stream1[Kafka: 注文イベント]
  end

  subgraph "Bounded Context: 在庫"
    OLTP2[DynamoDB: 在庫]
    Cache2[Redis: 在庫 cache]
  end

  subgraph "Bounded Context: 分析"
    OLAP[ClickHouse: 売上分析]
  end

  Stream1 --> OLAP
  OLTP1 -. CDC .-> OLAP
  OLTP2 -. CDC .-> OLAP

Source of Truth ─ 「正」はどこか

複数 DB を持つと、同じデータが複数箇所に存在する。例:

  • Postgres: 注文の現在状態
  • Kafka: 注文の event 履歴
  • ClickHouse: 注文の集計
  • Redis: 注文の hot data

どれが正なのか?

答えは「1 つだけ正がある

パターン
OLTP 中心OLTP DB(Postgres)
Event 中心(Event Sourcing)Stream / Event Log
主 DB + 派生 view主 DB のみ
混在は禁止

「Postgres も Kafka も両方が正」は破綻する。どちらか一方の DB を Source of Truth と決めて、他は派生(projection)として扱う

派生は再生可能でなければならない

graph TB
  S[Source of Truth<br/>Postgres or Kafka]

  S --> P1[Projection 1: ClickHouse]
  S --> P2[Projection 2: Elasticsearch]
  S --> P3[Projection 3: Redis]

  S -.再生可能.-> P1
  S -.再生可能.-> P2
  S -.再生可能.-> P3

  style S fill:#e1f5ff

Projection が壊れたら、Source から作り直せる べき。これが Pat Helland の Outside data is identifiable, immutable, stable の現代的な実装。

同期戦略

複数 DB の同期にはいくつかのパターンがある。

パターン 1:Outbox + Stream(推奨)

第 13 章で扱った Outbox Pattern を、複数 DB 連携に拡張する:

graph LR
  App[Application] --> P[(Postgres + Outbox)]
  P -. Outbox Publisher.-> K[Kafka]
  K --> C1[ClickHouse 同期 Consumer]
  K --> C2[Elasticsearch 同期 Consumer]
  K --> C3[Redis 同期 Consumer]
  • Aggregate の更新と event の publish が atomic(Outbox)
  • Kafka が中央の交換点
  • 各 DB は独立した consumer として同期

長所: 各 DB が独立、追加・削除が簡単、再生可能(Kafka を replay) 短所: 運用が複雑、Kafka 自体の管理が必要

パターン 2:CDC(Change Data Capture)

OLTP の WAL を読んで、変更を Stream に流す

graph LR
  P[(PostgreSQL)] -. WAL .-> CDC[Debezium]
  CDC --> K[Kafka]
  K --> C1[ClickHouse]
  K --> C2[Elasticsearch]

長所: アプリケーションコードに変更不要、漏れが起きない 短所: WAL の schema 進化に追従する必要、運用が独自

パターン 3:Application-level Dual Write(避ける)

// ❌ アンチパターン
async function placeOrder(order: Order) {
  await postgres.save(order);
  await kafka.publish(orderPlacedEvent(order));
  await redis.set(`order:${order.id}`, order);
}

第 13 章で見たとおり、Dual Write は本質的に壊れる。クラッシュタイミングで partial failure が起きる。

Saga ─ 複数 DB の “トランザクション”

1 つの business operation が複数 DB を更新する」場合、ACID Trans が組めない。これが Saga パターン

Saga の概念

複数のローカル Trans を 補償操作(compensation)の連鎖 で繋げる:

sequenceDiagram
  participant App as Application
  participant DB1 as Order DB
  participant DB2 as Inventory DB
  participant DB3 as Payment DB

  App->>DB1: TX1: 注文作成
  DB1-->>App: OK
  App->>DB2: TX2: 在庫予約
  DB2-->>App: OK
  App->>DB3: TX3: 支払い処理
  DB3--xApp: 失敗

  Note over App: 補償シーケンス開始
  App->>DB2: 補償: 在庫予約取消
  App->>DB1: 補償: 注文キャンセル

Choreography vs Orchestration

方式仕組み
Choreography各サービスが event を発火し、他が反応する。中心なし
Orchestration中央の Saga Orchestrator が指揮

Choreography: 疎結合、シンプル。だが失敗時の挙動を全体で見るのが難しい Orchestration: 中央集権、可視化容易。だが orchestrator 自体が複雑になる

複雑な Saga は Orchestration、シンプルな event 連携は Choreography が現代の標準。

ドメイン側の構え

複数 DB を持つことを前提にしたとき、ドメイン層はどう設計するか

構え 1:Aggregate は単一 DB に閉じる

Aggregate は 1 つの DB の Trans 内で完結 させる。複数 DB に跨る Aggregate は禁止。

// ✅ Aggregate は Postgres 内で閉じる
class Order {
  // すべてのフィールドが Postgres の同一 Trans で扱える
}

// 別 DB との同期は event 経由
class OrderRepository {
  async save(order: Order): Promise<void> {
    await tx.upsert('orders', ...);
    await tx.insert('outbox_events', orderPlacedEvent(order));
  }
}

構え 2:Read Model は別個に設計する

ドメイン層の Aggregate と、読み出し用の Read Model は別。

// 書き込み側: Aggregate
class Order { /* ... */ }

// 読み出し側: 用途別 View
class OrderListView {
  // ClickHouse から取得、集計済み
}

class OrderSearchResult {
  // Elasticsearch から取得、検索用
}

これは CQRS(Command Query Responsibility Segregation) の根拠。書き込みと読み出しを分離する。

構え 3:Repository を分割する

// 書き込み(Source of Truth)
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
}

// 読み出し(Projection)
interface OrderQueryService {
  search(criteria: OrderSearchCriteria): Promise<OrderSearchResult[]>;
  monthlyAggregate(month: Month): Promise<MonthlyAggregate>;
}

Repository は Source of Truth に対するインターフェース。Query Service は projection(複数 DB を読む可能性)。

Polyglot の罠

罠 1:「とりあえず別 DB」

「分析だから ClickHouse」「キャッシュだから Redis」と気軽に分けると、運用負担が膨らむ。本当に必要か を毎回問う。Postgres + Materialized View で済む場面も多い。

罠 2:データ整合性の隙間

A DB の更新が B DB に反映するまで数秒の遅延。その間にユーザーが画面遷移すると、自分の更新が反映されていない画面を見る。

対策:

  • 更新後の即時表示は client side で
  • Read your writes を必要な箇所だけ Strong consistent
  • UI で「反映に少し時間がかかります」と明示する

罠 3:Schema 進化の倍数効果

A DB の schema 変更 → CDC の構成変更 → B DB の schema 変更 → Consumer 全部更新。

1 つの schema 変更が複数システムに波及する。これを抑える設計が必要:

  • Avro / Protobuf + Schema Registry
  • 後方互換を死守する
  • Big bang ではなく段階的に

Polyglot 採用の判断軸

graph TB
  Q1{単一 DB で足りるか?}
  Q1 -->|Yes| Single[単一 DB を貫く]
  Q1 -->|No| Q2{Bounded Context は分かれているか?}
  Q2 -->|Yes| Q3{Source of Truth を 1 つに決められるか?}
  Q2 -->|No| Single2[まず Bounded Context を分ける]
  Q3 -->|Yes| Polyglot[Polyglot に進む]
  Q3 -->|No| Single3[整理してから]

  style Polyglot fill:#e1f5ff
  style Single fill:#e1ffe1
  style Single2 fill:#fff4e1
  style Single3 fill:#fff4e1

いきなり Polyglot にしない」が原則。シンプルから始めて、必要になったら分割する

この章の要点

  • Polyglot Persistence = データの使われ方に応じて DB を使い分ける
  • Source of Truth を 1 つに決める ── 他は派生として扱う
  • 同期戦略は Outbox + Stream または CDC。Dual Write は禁止
  • 複数 DB の “Trans” は Saga で補償操作の連鎖として実装
  • Aggregate は単一 DB に閉じる。Read Model は別個に設計(CQRS)
  • 罠: とりあえず別 DB、整合性の隙間、Schema 進化の倍数効果
  • いきなり Polyglot にしない。シンプルから始める

次章への問いかけ

複数 DB を持つ世界では、ドメイン駆動と特性駆動の両派が日常的に交わる。Aggregate を引きながら Partition Key を選び、Repository を書きながら projection を実装する。

次章で 両派を行き来する 設計判断のフレームを統合する。