mcpField
M
microservices-patterns

microservices-patterns

github.com/curiositech開発263133 回インストール2 日前に更新
リポジトリ

microservices-patterns について

独立したサービスとその連携を設計するためのアーキテクチャスキルです。分割、通信、データ所有権、耐障害性のパターンに加え、モノリスが適切な場合も扱います。

主な機能

  1. 01モノリスとマイクロサービスのトレードオフ評価
  2. 02境界づけられたコンテキストによるサービス境界の定義
  3. 03同期通信と非同期通信の選択
  4. 04Saga による分散トランザクションの設計
  5. 05サーキットブレーカーとバルクヘッドの適用
  6. 06CQRS とイベントソーシングの利用

活用シーン

  1. 01モノリスを段階的に分割する計画
  2. 02REST、gRPC、イベント通信の選択
  3. 03複数サービスにまたがる業務トランザクションの調整
  4. 04読み取りモデルと書き込みモデルの分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill microservices-patterns

インストール後、Claude Code で/microservices-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.claude-plugin/
plugin.json
references/
decomposition-strategies.md
communication-patterns.md

詳細

マイクロサービスパターン

システムを独立したサービスに分解し、それらが連携して確実に動作するようにする技法です。分解戦略、通信パターン、データの所有権、分散システムの障害が連鎖して全体的な障害に至るのを防ぐレジリエンスパターンを扱います。

使用する場面

次の用途に使用します:

  • モノリスを分解するかどうか、またどこから始めるかを判断する
  • 境界づけられたコンテキストとドメイン駆動設計を使ってサービス境界を設計する
  • 同期通信(REST/gRPC)と非同期通信(イベント)のどちらを選ぶか判断する
  • 分散トランザクションにサーガパターンを実装する
  • API ゲートウェイとバックエンド・フォー・フロントエンド(BFF)レイヤーを設計する
  • サーキットブレーカー、バルクヘッド、リトライパターンを適用する
  • イベントソーシングと CQRS を設計する
  • サービスディスカバリーとロードバランシングの戦略を立てる

次の用途には使用しません:

  • モノリス内部のアーキテクチャ(database-design-patterns、api-architectを使用)
  • サーバーレス関数の設計とデプロイ
  • Kubernetes のインフラストラクチャとデプロイ設定(terraform-iac-expertを使用)
  • サービスメッシュの設定(Istio、Linkerd)— 言及することはあっても、中心的な対象ではありません
  • 単一サービスのパフォーマンス最適化(performance-profilingを使用)

中核となる判断:モノリス、マイクロサービス、モジュラーモノリスのどれを選ぶか

flowchart TD
    Start[New project or architecture review] --> TeamSize{Team size?}
    TeamSize -->|1-8 engineers| Small[Modular monolith first]
    TeamSize -->|9-25| Medium{Domain complexity?}
    TeamSize -->|25+| Large{Independent deploy needed?}

    Small --> S1[Build well-factored modules with clear boundaries]
    S1 --> S2{Growing pains?}
    S2 -->|No| S1
    S2 -->|Yes: deploy conflicts, team coupling| Extract[Extract services at natural seams]

    Medium -->|Simple, few domains| Modular[Modular monolith]
    Medium -->|Complex, many bounded contexts| MicroQ{Org structure?}
    MicroQ -->|Teams align to domains| Micro[Microservices]
    MicroQ -->|Teams are cross-functional| Modular

    Large -->|Yes, teams blocked waiting for each other| Micro
    Large -->|No, deploys are coordinated| Modular

    Micro --> Check{Check: do services deploy independently?}
    Check -->|No, they must release together| Problem[You have a distributed monolith]
    Check -->|Yes| Proceed[Proceed with microservices]
マイクロサービスのコストに見合う場合

マイクロサービスの利点は、独立したスケーリング、独立したデプロイ、技術選択の多様性、障害の分離です。一方で、分散システムの複雑さ、結果整合性、運用上のオーバーヘッド、ネットワーク遅延というコストがあります。

マイクロサービスが適しているのは、次のような場合です:

  • システムの各部分で、スケーリング要件が大きく異なる
  • 単一のコードベースによる調整のオーバーヘッドが生じるほど、チームの規模が大きい
  • サービス境界が頻繁に変わらないほど、ドメインが安定している
  • チームに分散システムを管理する運用上の成熟度(オブザーバビリティ、自動デプロイ、オンコール対応)がある

毎日機能をリリースするエンジニア4人のスタートアップは、ほぼ間違いなくマイクロサービスを構築すべきではありません。一方、チェックアウトチームがカタログチームのリリースを待たされている、エンジニア200人の企業は、まさにマイクロサービスが適した状況です。


サービスの分解

境界づけられたコンテキスト

境界づけられたコンテキストとは、ドメインモデルが適用される明確な境界です。境界内では、言語、概念、ルールに一貫性があります。境界を越える際には、明示的な変換が行われます。

Order Service:
  - "customer" = { id, shippingAddress, paymentMethod }
  - "product" = { id, price, quantity }

Catalog Service:
  - "product" = { id, name, description, images, attributes, category }
  - "customer" — not a concept here at all

Recommendation Service:
  - "customer" = { id, browsingHistory, purchaseHistory }
  - "product" = { id, category, tags }

同じ言葉(「製品」)でも、コンテキストごとに意味が異なります。これは正しい状態です。すべてのサービスで単一の共有モデルを無理に使うと、密結合が生じます。

ストラングラーフィグパターン

モノリスを安全に分解する方法です。ファサードを経由してトラフィックをルーティングし、機能を少しずつ切り出します。一度にすべてを書き換えることはありません。

flowchart LR
    Client --> Facade[API Gateway / Facade]
    Facade --> Monolith[(Monolith)]
    Facade --> NewService[New Service]

    subgraph "Phase 1: Identify seam"
        Monolith
    end
    subgraph "Phase 2: Route new traffic"
        NewService
    end
    subgraph "Phase 3: Migrate & delete"
        Monolith -->|Sunset| Deleted[Deleted]
    end

手順:

  1. 継ぎ目を特定する — 入出力が明確で、内部依存が最小限のモノリス内のモジュールを見つけます
  2. 新しいサービスを立ち上げる — 同じ機能を独立して実装します
  3. 新しいトラフィックを新しいサービスにルーティングする。古いトラフィックは引き続きモノリスに送ります
  4. 古いデータとトラフィックを徐々に移行する
  5. 確信が持てたら、モノリスのコードを削除する

モノリス全体を一度に切り出そうとしてはいけません。一度に一つの継ぎ目を扱います。


通信パターン

同期通信と非同期通信
flowchart TD
    Decision[Choosing communication] --> Need{Does caller need an immediate response?}
    Need -->|Yes, and can fail if downstream is down| Sync[Synchronous: REST or gRPC]
    Need -->|No, or needs to tolerate downstream outages| Async[Asynchronous: events/messages]

    Sync --> SyncQ{Protocol?}
    SyncQ -->|CRUD operations, public APIs| REST[REST/HTTP]
    SyncQ -->|Internal, high throughput, streaming| GRPC[gRPC]

    Async --> AsyncQ{Pattern?}
    AsyncQ -->|Fire and forget, fan-out| Events[Event bus: Kafka, NATS, SNS]
    AsyncQ -->|Work queue, at-least-once delivery| Queue[Message queue: SQS, RabbitMQ]
    AsyncQ -->|Multi-step transaction coordination| Saga
サーキットブレーカー

低速または停止した下流サービスによって、呼び出し元まで停止するのを防ぎます。

States:
  CLOSED (normal)    → requests pass through
  OPEN (tripped)     → requests fail fast without calling downstream
  HALF-OPEN (probe)  → one request allowed through to test recovery

Transitions:
  CLOSED  → OPEN:      failure threshold exceeded (e.g., 5 failures in 10 seconds)
  OPEN    → HALF-OPEN: after timeout (e.g., 30 seconds)
  HALF-OPEN → CLOSED:  probe request succeeds
  HALF-OPEN → OPEN:    probe request fails
// Example using opossum (Node.js circuit breaker library)
const CircuitBreaker = require('opossum');

const options = {
  timeout: 3000,           // If function takes longer than 3s, trigger failure
  errorThresholdPercentage: 50,  // Open circuit when 50% of requests fail
  resetTimeout: 30000,     // Try again after 30s
};

const breaker = new CircuitBreaker(callPaymentService, options);

breaker.on('open', () => console.log('Circuit open — payment service unreachable'));
breaker.on('halfOpen', () => console.log('Testing payment service recovery'));
breaker.on('close', () => console.log('Circuit closed — payment service recovered'));

// Fallback when circuit is open
breaker.fallback(() => ({ status: 'pending', message: 'Payment queued for retry' }));
バルクヘッド

障害を分離します。各下流サービスに専用のスレッドプール/接続プールを割り当て、一つの低速なサービスがすべてのリソースを使い果たすのを防ぎます。

// Naive: single shared pool — one slow dependency starves everything
const pool = new DatabasePool({ max: 50 });

// Bulkhead: separate pools per service
const pools = {
  payments: new Pool({ max: 10 }),    // Max 10 concurrent payment calls
  catalog: new Pool({ max: 20 }),     // Catalog can use more
  notifications: new Pool({ max: 5 }), // Limit low-priority work
};

サーガパターン:分散トランザクション

サーガは、2フェーズコミットが必要でコストも高い分散 ACID トランザクションの代わりに、一連のローカルトランザクションを使います。各トランザクションはイベントまたはメッセージを発行して次のステップを開始します。あるステップが失敗した場合は、補償トランザクションによって前のステップを取り消します。

オーケストレーション型サーガ

中央のオーケストレーター(サーガコーディネーター)が各サービスに実行内容を指示し、失敗時には補正コマンドを発行して対処します。

sequenceDiagram
    participant O as Order Saga Orchestrator
    participant OS as Order Service
    participant IS as Inventory Service
    participant PS as Payment Service
    participant NS as Notification Service

    O->>OS: CreateOrder
    OS-->>O: OrderCreated

    O->>IS: ReserveInventory
    IS-->>O: InventoryReserved

    O->>PS: ProcessPayment
    alt Payment succeeds
        PS-->>O: PaymentProcessed
        O->>NS: SendConfirmation
        NS-->>O: NotificationSent
        O->>OS: MarkOrderComplete
    else Payment fails
        PS-->>O: PaymentFailed
        O->>IS: ReleaseInventory  [compensating transaction]
        O->>OS: CancelOrder       [compensating transaction]
    end

オーケストレーションを使う場面:ステップが多く、条件分岐もある複雑なワークフローです。サーガの状態とエラー処理が明示的に一元管理されます。確認する場所が1か所なので可観測性は高まりますが、知識が過度に集中する中央コーディネーターが生まれます。

コレオグラフィー型サーガ

中央コーディネーターはありません。各サービスがイベントをリッスンして実行内容を判断し、その後、自身のイベントを発行します。

sequenceDiagram
    participant OS as Order Service
    participant IS as Inventory Service
    participant PS as Payment Service
    participant NS as Notification Service
    participant EB as Event Bus

    OS->>EB: OrderCreated
    EB->>IS: OrderCreated (consumed)
    IS->>EB: InventoryReserved
    EB->>PS: InventoryReserved (consumed)

    alt Payment succeeds
        PS->>EB: PaymentProcessed
        EB->>NS: PaymentProcessed (consumed)
        NS->>EB: NotificationSent
        EB->>OS: NotificationSent (consumed)
        OS->>OS: MarkOrderComplete
    else Payment fails
        PS->>EB: PaymentFailed
        EB->>IS: PaymentFailed (consumed)
        IS->>IS: ReleaseInventory [compensating]
        IS->>EB: InventoryReleased
        EB->>OS: InventoryReleased (consumed)
        OS->>OS: CancelOrder [compensating]
    end

コレオグラフィーを使う場面:ステップが少ない、よりシンプルなワークフローです。各サービスの自律性が高く、全体のフローを把握しているサービスはありません。可観測性は低く(サービスをまたいで追跡する必要があります)なりますが、疎結合になります。


アンチパターン:分散モノリス

初心者:アプリケーションを8つのサービスに分割しましたが、データベーススキーマを共有していたり、バージョンが一致しないと動作しない同期呼び出しを行っていたりするため、リリースのたびに8つすべてを同時にデプロイする必要があります。

エキスパート:同時にデプロイしなければならないマイクロサービスは、マイクロサービスではありません。両方のアーキテクチャの欠点をすべて持ち、利点を何も持たない分散モノリスです。本当のマイクロサービスは独立してデプロイでき、後方互換性のあるAPIとイベントスキーマによってバージョンのずれを許容し、それぞれが自身のデータを完全に所有します。「サービスBに触れずにサービスAをデプロイできますか?」という問いに「はい」と答えられないなら、分割は完了していません。

検出方法:デプロイ手順書に「これらのサービスをこの順番でデプロイする」と書かれています。異なるバージョンのサービスを実行すると、統合テストが失敗します。チーム間でサービスの境界をまたいでリリース日を調整しています。


アンチパターン:同期呼び出しチェーン

初心者:ユーザーのリクエストが API Gateway に届き、Order Service から Inventory Service を呼び出し、さらに Warehouse Service、Shipping Service を呼び出します。すべて同期 HTTP です。

エキスパート:同期呼び出しが4つ連なると、レイテンシが増大し、可用性も低下します。各サービスの可用性が99.9%でも、4つのチェーンでは可用性が99.6%になり、月に3.5時間のダウンタイムが発生します。レイテンシも積み重なります。各サービスが50msなら、最低でも200msにネットワークのオーバーヘッドが加わります。ユーザーを待たせる必要のない処理には非同期イベントを使い、すべての同期呼び出しにサーキットブレーカーパターンを適用してください。チェーンが2〜3ホップを超える場合は、データの所有権を見直してください。呼び出し元は、本来自身が所有すべきデータを持っていない可能性があります。

検出方法:分散トレースのリクエストウォーターフォールで、サービスAがBを待ち、BがCを待っていることが確認できます。p99レイテンシがp50より大幅に悪化しています(テールレイテンシの連鎖)。


アンチパターン:共有データベース

初心者:サービス間のデータアクセスの複雑さを避けるため、マイクロサービスでPostgreSQLデータベースを共有しています。

エキスパート:共有データベースは、ストレージ層での密結合です。スキーマを変更するたびに、そのテーブルにアクセスするすべてのサービスとの調整が必要になります。あるサービスの遅いクエリによって、別のサービスが必要とする行がロックされることがあります。データアクセスパターンが異なるサービスを個別にスケーリングできません。各サービスがスキーマ、インデックスなどを含む自身のデータストアを所有する必要があります。サービス間のデータアクセスには、データを所有するサービスのAPIまたはイベントを使います。もちろん、サービスの境界をまたいだJOINはできません。これこそ、明確なデータ所有権を確立するための制約です。

検出方法:サービスBが共有テーブルのスキーマを変更したため、サービスAのテストが失敗します。各サービスが同じデータベース接続認証情報を使っています。スキーマのマイグレーションには、複数のサービスを同時に停止する必要があります。


CQRSとイベントソーシング

CQRS(コマンドクエリ責務分離)

書き込みモデル(コマンド。不変条件を適用します)と、読み取りモデル(クエリ。表示に最適化されています)を分離します。

Write side:                          Read side:
  POST /orders → OrderService          GET /orders/{id} → OrderQueryService
  Validates business rules             Materialized view, denormalized
  Writes to order aggregate            Updated from events
  Emits OrderPlaced event              No business logic, just data

読み取りパターンが書き込みパターンと大きく異なる場合、CQRSは有効です。たとえば、書き込みでは複雑なビジネスルールを検証する一方、読み取りでは複数の集約にまたがる非正規化ビューが必要になる場合です。

イベントソーシング

現在の状態を保存する代わりに、その状態を生み出したイベントのシーケンスを保存します。現在の状態は、イベントをリプレイして導出します。

// Traditional: store current state
await db.update('orders', { id, status: 'SHIPPED', shippedAt: new Date() });

// Event sourcing: store what happened
await eventStore.append('order-' + id, {
  type: 'OrderShipped',
  payload: { orderId: id, carrier: 'FedEx', trackingNumber: '9400...' },
  timestamp: new Date(),
  version: 4,  // optimistic concurrency control
});

// Derive current state by replaying
async function getOrderState(orderId) {
  const events = await eventStore.getEvents('order-' + orderId);
  return events.reduce(applyEvent, { status: null, items: [], history: [] });
}

イベントソーシングでは、完全な監査ログ、時間をさかのぼる機能(任意の時点までリプレイ)、および過去のイベントから新しい読み取りモデルを導出する機能が得られます。トレードオフとして、クエリが難しくなり(プロジェクションを使用する必要があります)、イベントスキーマの進化には慎重なバージョン管理が必要です。


API ゲートウェイと BFF

API ゲートウェイの責務
  • 認証/認可:JWT を検証し、転送前にスコープを確認します
  • レート制限:クライアントごと、エンドポイントごとの制限を設定します
  • リクエストのルーティング:パス/ヘッダーに基づいて適切なサービスにルーティングします
  • プロトコル変換:外部の REST を内部の gRPC に、またはその逆に変換します
  • レスポンスの集約:複数のサービスに並行してリクエストし、結果を統合します
  • SSL 終端:外部では HTTPS、内部では HTTP を処理します
フロントエンド向けバックエンド(BFF)

汎用 API ゲートウェイを 1 つだけ使うと問題が生じます。モバイルクライアントには小さなペイロードが必要で、Web クライアントには豊富なデータが必要ですが、ゲートウェイはすべてのクライアントに対して妥協点を探ることになります。BFF では、クライアントの種類ごとに個別のゲートウェイを作成します。

Mobile App → BFF-Mobile → [User Service, Order Service]
                           (small payloads, battery-conscious)

Web App → BFF-Web → [User Service, Order Service, Recommendation Service]
                     (rich data, aggregated views)

Third-party API → Public API Gateway → (rate-limited, versioned, documented)

サービスディスカバリー

クライアントサイドディスカバリー

クライアントがサービスレジストリ(Consul、Eureka)に問い合わせ、自身で負荷分散します。

// Client-side: ask registry, then call directly
const instances = await consul.health.service({ service: 'payment-service', passing: true });
const instance = loadBalance(instances);
const url = `http://${instance.Service.Address}:${instance.Service.Port}`;
await fetch(`${url}/api/charge`);
サーバーサイドディスカバリー

ロードバランサー(nginx、AWS ALB、Kubernetes Service)がディスカバリーを処理します。クライアントはロードバランサーを呼び出し、ロードバランサーが正常なインスタンスにルーティングします。クライアントにとってはよりシンプルです。Kubernetes ではこれを使用してください(Kubernetes Services が代わりに処理します)。


Saga オーケストレーション:ステートマシンの設計

自作する場合とフレームワークを使う場合

本番環境の Saga には Temporal.io または AWS Step Functions を使用してください。 耐久性のある実行、クラッシュからの復旧、可視性をこれらが提供します。独自の Saga エンジンを構築するのが妥当なのは、次の場合に限られます。(a)Temporal の永続化オーバーヘッドでは実現できない 10ms 未満のステップレイテンシーが必要な場合、または(b)組織が別のインフラストラクチャ依存関係を採用できない場合です。

独自に構築する場合は、次の設計を案内してください。

状態モデル

すべての Saga は、ステップ遷移のたびに状態レコードを永続化します。これが復旧メカニズムです。コーディネーターがクラッシュした場合、最後に永続化された状態を読み取り、そこから再開します。

interface SagaState {
  sagaId: string;
  status: 'running' | 'compensating' | 'completed' | 'failed';
  currentStepIndex: number;
  completedSteps: string[];         // Step names that succeeded
  compensatedSteps: string[];       // Step names that were compensated
  context: Record<string, unknown>; // Accumulates results from each step
  startedAt: string;                // ISO timestamp
  updatedAt: string;
  failureReason?: string;
}

これをデータベースに永続化してください(Postgres の JSONB カラムで問題ありません)。Redis やメモリ内には保存しないでください。コーディネーターがクラッシュしても、状態が保持される必要があります。

実行ルール
  1. 順方向の実行:ステップを順番に実行します。各ステップが成功した後、currentStepIndex++ で状態を更新し、そのステップの結果を context にマージして永続化します。永続化してから進める順序が重要です。ステップの実行後、永続化の前にプロセスがクラッシュした場合、復旧時にステップが再実行されます。ステップは必ず冪等でなければなりません。

  2. 失敗時に補償を開始:ステップが失敗した場合、status: 'compensating' に設定し、最後に完了したステップから逆順に補償処理を実行します。completedSteps に含まれ、かつ compensatedSteps にまだ含まれていないステップだけを補償します。

  3. 補償も永続化する:各補償が成功した後、そのステップ名を compensatedSteps に追加して永続化します。補償中にコーディネーターがクラッシュした場合、中断した箇所から補償を再開します。ステップを二重に補償することはありません。

  4. ステップごとのタイムアウト:各ステップで独自のタイムアウトを宣言します。支払いのキャプチャには 30 秒、メール通知には 5 秒が必要な場合があります。ステップの実行を Promise.race([step.command(context), timeout(step.timeoutMs)]) でラップします。タイムアウトした場合は失敗として扱い、補償を開始します。

  5. 再開ロジック:コーディネーターの起動時に、status: 'running' または status: 'compensating' の Saga を検索します。running の場合は、currentStepIndex のステップを再実行します(冪等なので安全です)。compensating の場合は、未補償の最初のステップから逆順に補償を続けます。

例:注文 Saga
Steps (forward):
  1. reserveInventory  → compensate: releaseInventory
  2. chargePayment     → compensate: refundPayment
  3. shipOrder         → compensate: cancelShipment
  4. sendConfirmation  → compensate: sendCancellationEmail

If chargePayment fails:
  → compensate releaseInventory (only step 1 completed)
  → set status: 'failed', record failureReason

If shipOrder fails:
  → compensate refundPayment (step 2)
  → compensate releaseInventory (step 1)
  → reverse order matters: refund before releasing inventory
やってはいけないこと
  • インメモリの状態マシンを使わないでください — プロセスの終了とともに消失し、サガが実行途中で止まっても復旧できなくなります。
  • 補償処理を順方向に行わないでください — 在庫を解放した後に返金すると、別の顧客がその在庫を確保している可能性があります。逆順に処理することで、論理的な整合性を保てます。
  • 無期限に再試行しないでください — 補償処理も失敗する可能性があります。補償処理を3回再試行した後は、人間による対応を求めるアラートを発してください。compensating 状態のまま何時間も止まっているサガは、明確に失敗するサガよりも深刻です。
  • 冪等性を省略しないでください — クラッシュからの復旧の仕組みは、すべて冪等性に依存しています。同じサガ ID で reserveInventory を2回呼び出した場合、二重に在庫を確保するのではなく、同じ結果を返す必要があります。

参考資料

  • references/communication-patterns.md — 以下を参照してください:REST と gRPC の選択マトリックス、非同期メッセージングブローカー(Kafka、RabbitMQ、SQS)の比較、冪等性のパターン、サガのコレオグラフィとオーケストレーションのトレードオフ、CQRS の読み取りモデルのパターン
  • references/decomposition-strategies.md — 以下を参照してください:境界づけられたコンテキストの特定、ストラングラーフィグの実装手順、ドメイン駆動の分解手法、チームトポロジーとの整合、データベース分解のパターン、データ移行戦略

コメント