LESSON 40分

ストーリー

佐藤CTO
境界は見えてきた。だが、一気に分割するのは危険だ
佐藤CTO
段階的に進めるアプローチを選ぶことで、リスクを最小化しながら確実に前進できる

サービス分割のアプローチ

Strangler Fig パターン(詳細)

graph TD
    subgraph P1["Phase 1: ファサード追加"]
        GW1["API Gateway\nリクエストをルーティング"] --> NewSvc["新サービス\n通知"]
        GW1 --> Mono1["モノリス\n残りの機能"]
    end
    subgraph P2["Phase 2: 段階的移行"]
        GW2["API Gateway"] --> N2["通知"]
        GW2 --> O2["注文"]
        GW2 --> P2a["決済"]
        GW2 --> Mono2["モノリス\n縮小中"]
    end
    subgraph P3["Phase 3: 完了"]
        GW3["API Gateway"] --> N3["通知"]
        GW3 --> O3["注文"]
        GW3 --> P3a["決済"]
        GW3 --> I3["在庫"]
        GW3 --> C3["顧客"]
    end

    style P1 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style P2 fill:#fef3c7,stroke:#d97706,color:#92400e
    style P3 fill:#d1fae5,stroke:#059669,color:#065f46
    style GW1 fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6
    style GW2 fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6
    style GW3 fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6
    style NewSvc fill:#d1fae5,stroke:#059669,color:#065f46
    style Mono1 fill:#fee2e2,stroke:#dc2626,color:#991b1b
    style Mono2 fill:#fef3c7,stroke:#d97706,color:#92400e
    style N2 fill:#d1fae5,stroke:#059669,color:#065f46
    style O2 fill:#d1fae5,stroke:#059669,color:#065f46
    style P2a fill:#d1fae5,stroke:#059669,color:#065f46
    style N3 fill:#d1fae5,stroke:#059669,color:#065f46
    style O3 fill:#d1fae5,stroke:#059669,color:#065f46
    style P3a fill:#d1fae5,stroke:#059669,color:#065f46
    style I3 fill:#d1fae5,stroke:#059669,color:#065f46
    style C3 fill:#d1fae5,stroke:#059669,color:#065f46

分割順序の決め方

優先度基準理由
1変更頻度が高いモジュール独立デプロイの効果が最大
2依存が少ないモジュール分離が容易でリスクが低い
3チームが明確なモジュール所有権が自然に決まる
4パフォーマンス要件が異なるモジュール独立スケーリングの恩恵

データベース分割戦略

パターン1: Database per Service

graph TD
    subgraph Before["Before: 共有DB"]
        SharedDB["共有 PostgreSQL"]
        SharedDB --> T1["orders"]
        SharedDB --> T2["products"]
        SharedDB --> T3["users"]
    end
    subgraph After["After: DB per Service"]
        DB1["Order DB\nPostgres"]
        DB2["Product DB\nPostgres"]
        DB3["User DB\nPostgres"]
    end

    style Before fill:#fee2e2,stroke:#dc2626,color:#991b1b
    style After fill:#d1fae5,stroke:#059669,color:#065f46
    style SharedDB fill:#fee2e2,stroke:#dc2626,color:#991b1b
    style T1 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style T2 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style T3 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style DB1 fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style DB2 fill:#d1fae5,stroke:#059669,color:#065f46
    style DB3 fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6

パターン2: 段階的分離(推奨)

graph TD
    S1["Step 1\nSchema分離(同一DB内)"] --> S2["Step 2\nView/Synonym で互換維持"]
    S2 --> S3["Step 3\nAPI経由に切り替え"]
    S3 --> S4["Step 4\n物理DB分離"]

    S1 -.- D1["PostgreSQL内で\norder / product / user\nスキーマを分離"]
    S2 -.- D2["orders_view →\nproduct_schema.products\nREAD互換を維持"]
    S3 -.- D3["Order Service\n--HTTP/gRPC-->\nProduct Service"]
    S4 -.- D4["各サービス専用の\nDBインスタンスに分離"]

    style S1 fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style S2 fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style S3 fill:#fef3c7,stroke:#d97706,color:#92400e
    style S4 fill:#d1fae5,stroke:#059669,color:#065f46
    style D1 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style D2 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style D3 fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style D4 fill:#f3f4f6,stroke:#9ca3af,color:#374151

データ整合性の課題

// モノリス時代: JOINで簡単に取得
// SELECT o.*, p.name FROM orders o JOIN products p ON o.product_id = p.id

// マイクロサービス: API Composition パターン
class OrderDetailComposer {
  async getOrderWithProducts(orderId: string): Promise<OrderDetail> {
    // 並行して2つのサービスからデータ取得
    const [order, products] = await Promise.all([
      this.orderService.getOrder(orderId),
      this.productService.getProducts(order.items.map((i) => i.productId)),
    ]);

    return {
      ...order,
      items: order.items.map((item) => ({
        ...item,
        product: products.find((p) => p.id === item.productId),
      })),
    };
  }
}

移行中のデータ同期

Change Data Capture (CDC)

graph LR
    MDB["モノリス<br/>DB"] -->|WAL| DEB["Debezium<br/>(CDC)"] -->|Kafka| NDB["新サービス<br/>DB"]

    classDef db fill:#e8f4fd,stroke:#2196f3,color:#333
    classDef mid fill:#fff3cd,stroke:#f0ad4e,color:#333
    class MDB,NDB db
    class DEB mid

旧DBの変更をリアルタイムに新サービスのDBへ反映し、移行期間中のデータ整合性を維持します。

Dual Write の危険性

❌ 避けるべき: Dual Write
Service → Write to DB A → Write to DB B
         (成功)         (失敗したら?)

✅ 推奨: CDC or Outbox パターン
Service → Write to DB A → CDC → DB B
         (1箇所書き込み)  (自動同期)

まとめ

ポイント内容
Strangler Fig段階的にモノリスを置き換える
分割順序変更頻度・依存度・チーム構造で判断
DB分離Schema分離→View→API→物理分離の順
データ同期CDCで移行期間中の整合性を維持

チェックリスト

  • Strangler Fig パターンのフェーズを説明できる
  • サービス分割の優先順位を判断できる
  • DB分離の段階的アプローチを理解した
  • CDC によるデータ同期の仕組みを理解した

次のステップへ

次はサービス間のAPI契約設計を学びます。


推定読了時間: 40分