🧩

マイクロサービスアーキテクチャ入門 - 設計原則と実装パターン

20分 で読める | 2025.12.02

マイクロサービスアーキテクチャは、大規模なアプリケーションを独立した小さなサービスに分割する設計手法です。Netflix、Amazon、Uberなど多くの企業が採用し、現代のクラウドネイティブ開発の主流となっています。本記事では、マイクロサービスの基本概念から実装パターンまで、体系的に解説します。

モノリスとマイクロサービスの比較

モノリシックアーキテクチャ

flowchart TB
    subgraph Mono["モノリシックアーキテクチャ - 単一のアプリケーション"]
        subgraph Modules["機能モジュール"]
            M1["ユーザー管理"]
            M2["商品管理"]
            M3["注文管理"]
            M4["決済処理"]
        end
        DB["共有データベース"]
    end

    Modules --> DB
    Note["単一のデプロイ単位"]

マイクロサービスアーキテクチャ

flowchart TB
    Gateway["API Gateway / Service Mesh"]

    subgraph UserSvc["ユーザーサービス"]
        US["ユーザー<br/>サービス"]
        UDB["Users DB"]
        US --> UDB
    end

    subgraph ProductSvc["商品サービス"]
        PS["商品<br/>サービス"]
        PDB["Products DB"]
        PS --> PDB
    end

    subgraph OrderSvc["注文サービス"]
        OS["注文<br/>サービス"]
        ODB["Orders DB"]
        OS --> ODB
    end

    subgraph PaymentSvc["決済サービス"]
        PaS["決済<br/>サービス"]
        PaDB["Payments DB"]
        PaS --> PaDB
    end

    Gateway --> UserSvc
    Gateway --> ProductSvc
    Gateway --> OrderSvc
    Gateway --> PaymentSvc

比較表

観点モノリスマイクロサービス
デプロイ全体をまとめてサービス単位で独立
スケーリング全体をスケール必要なサービスのみ
技術スタック統一が必要サービスごとに選択可能
障害影響全体に波及該当サービスに限定
開発チーム全員が全体を把握サービスごとに専任可能
複雑性コード内に集中インフラ・運用に分散

マイクロサービスの設計原則

1. 単一責任の原則(Single Responsibility)

各サービスは1つのビジネス機能に集中します。

// 悪い例: 1つのサービスに複数の責務
class UserOrderService {
  createUser() { /* ... */ }
  updateUser() { /* ... */ }
  createOrder() { /* ... */ }      // 異なるドメイン
  processPayment() { /* ... */ }   // 異なるドメイン
}

// 良い例: 責務ごとにサービスを分離
// user-service
class UserService {
  createUser() { /* ... */ }
  updateUser() { /* ... */ }
  getUserById() { /* ... */ }
}

// order-service
class OrderService {
  createOrder() { /* ... */ }
  getOrdersByUser() { /* ... */ }
}

2. データの独立性(Database per Service)

各サービスは自身のデータストアを持ち、他サービスのデータベースに直接アクセスしません。

flowchart TB
    subgraph Anti["❌ アンチパターン"]
        A1["Service A"]
        B1["Service B"]
        SharedDB["共有DB"]
        A1 -->|直接アクセス| SharedDB
        B1 -->|直接アクセス| SharedDB
    end

    subgraph Good["✅ 推奨パターン"]
        A2["Service A"]
        B2["Service B"]
        DBA["DB A"]
        DBB["DB B"]
        A2 --> DBA
        B2 --> DBB
        A2 <-->|API経由| B2
    end

    style Anti fill:#fee,stroke:#f00
    style Good fill:#efe,stroke:#0f0

3. 疎結合(Loose Coupling)

サービス間の依存を最小限に抑え、インターフェースを通じてのみ通信します。

// order-service が user-service と通信する例
interface UserClient {
  getUserById(userId: string): Promise<User>;
}

class OrderService {
  constructor(private userClient: UserClient) {}

  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
    // インターフェースを通じて他サービスと通信
    const user = await this.userClient.getUserById(userId);

    if (!user.isActive) {
      throw new Error('User is not active');
    }

    return this.orderRepository.create({
      userId,
      items,
      createdAt: new Date(),
    });
  }
}

4. 高凝集(High Cohesion)

関連する機能は同じサービス内にまとめます。

flowchart TB
    subgraph ECommerce["E-Commerce Domain"]
        subgraph UserCtx["User Context"]
            U1["User"]
            U2["Profile"]
            U3["Address"]
            U4["Authentication"]
        end

        subgraph OrderCtx["Order Context"]
            O1["Order"]
            O2["OrderItem"]
            O3["Payment"]
            O4["Shipping"]
        end

        subgraph InvCtx["Inventory Context"]
            I1["Product"]
            I2["Stock"]
            I3["Warehouse"]
            I4["Supplier"]
        end
    end

サービス間通信パターン

1. 同期通信(REST / gRPC)

即座にレスポンスが必要な場合に使用します。

// REST API での同期通信
class ProductClient {
  private baseUrl = 'http://product-service:8080';

  async getProduct(productId: string): Promise<Product> {
    const response = await fetch(`${this.baseUrl}/products/${productId}`, {
      headers: {
        'Content-Type': 'application/json',
        'X-Request-ID': generateRequestId(),  // トレーシング用
      },
      signal: AbortSignal.timeout(5000),  // タイムアウト設定
    });

    if (!response.ok) {
      throw new ProductServiceError(response.status);
    }

    return response.json();
  }
}
// gRPC での同期通信(protocol buffers)
syntax = "proto3";

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product);
  rpc ListProducts(ListProductsRequest) returns (stream Product);
}

message GetProductRequest {
  string product_id = 1;
}

message Product {
  string id = 1;
  string name = 2;
  int32 price = 3;
  int32 stock = 4;
}

2. 非同期通信(メッセージキュー)

結果整合性が許容される場合や、処理の分離が必要な場合に使用します。

// イベント駆動アーキテクチャ
interface OrderCreatedEvent {
  eventType: 'ORDER_CREATED';
  orderId: string;
  userId: string;
  items: OrderItem[];
  totalAmount: number;
  timestamp: Date;
}

// order-service: イベント発行
class OrderService {
  async createOrder(order: CreateOrderDto): Promise<Order> {
    const created = await this.orderRepository.create(order);

    // イベントを発行(他サービスが購読)
    await this.eventBus.publish<OrderCreatedEvent>({
      eventType: 'ORDER_CREATED',
      orderId: created.id,
      userId: created.userId,
      items: created.items,
      totalAmount: created.totalAmount,
      timestamp: new Date(),
    });

    return created;
  }
}

// inventory-service: イベント購読
class InventoryEventHandler {
  @Subscribe('ORDER_CREATED')
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    for (const item of event.items) {
      await this.inventoryService.decrementStock(item.productId, item.quantity);
    }
  }
}

通信パターンの選択基準

パターンユースケース特徴
RESTCRUD操作、シンプルなAPI広く普及、デバッグ容易
gRPC高性能が必要、型安全性重視高速、ストリーミング対応
メッセージキュー非同期処理、イベント駆動疎結合、スケーラビリティ
GraphQLクライアント主導のデータ取得柔軟なクエリ、オーバーフェッチ防止

API Gateway パターン

クライアントとマイクロサービス間の単一エントリーポイントを提供します。

flowchart TB
    Client["Client"] --> Gateway

    subgraph Gateway["API Gateway"]
        G1["認証/認可"]
        G2["レート制限"]
        G3["リクエストルーティング"]
        G4["レスポンス集約"]
        G5["プロトコル変換"]
    end

    Gateway --> US["User Service"]
    Gateway --> OS["Order Service"]
    Gateway --> PS["Product Service"]
// API Gateway でのルーティング設定例(Express + http-proxy-middleware)
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

// 認証ミドルウェア
app.use(authMiddleware);

// サービスごとのルーティング
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:8080',
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

app.use('/api/orders', createProxyMiddleware({
  target: 'http://order-service:8080',
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
}));

app.use('/api/products', createProxyMiddleware({
  target: 'http://product-service:8080',
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' },
}));

障害対策パターン

1. サーキットブレーカー

障害が発生しているサービスへの呼び出しを一時的に遮断します。

enum CircuitState {
  CLOSED,     // 正常動作
  OPEN,       // 遮断中
  HALF_OPEN,  // 回復確認中
}

class CircuitBreaker {
  private state = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime: Date | null = null;

  private readonly failureThreshold = 5;
  private readonly resetTimeout = 30000; // 30秒

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new CircuitBreakerOpenError();
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess(): void {
    this.failureCount = 0;
    this.state = CircuitState.CLOSED;
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = new Date();

    if (this.failureCount >= this.failureThreshold) {
      this.state = CircuitState.OPEN;
    }
  }

  private shouldAttemptReset(): boolean {
    return Date.now() - (this.lastFailureTime?.getTime() ?? 0) > this.resetTimeout;
  }
}

2. リトライパターン

一時的な障害に対して、指数バックオフでリトライします。

async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxRetries: number;
    baseDelay: number;
    maxDelay: number;
  }
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      if (attempt === options.maxRetries) break;

      // 指数バックオフ + ジッター
      const delay = Math.min(
        options.baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        options.maxDelay
      );

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw lastError!;
}

// 使用例
const user = await withRetry(
  () => userClient.getUserById(userId),
  { maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }
);

3. バルクヘッドパターン

リソースを分離し、障害の影響範囲を限定します。

// スレッドプール/接続プールの分離
const pools = {
  userService: new ConnectionPool({ maxConnections: 10 }),
  orderService: new ConnectionPool({ maxConnections: 20 }),
  paymentService: new ConnectionPool({ maxConnections: 5 }),
};

// 1つのサービスが過負荷でも、他のサービスへの影響を防ぐ

分散トランザクション

Saga パターン

複数サービスにまたがるトランザクションを、一連のローカルトランザクションとして実装します。

sequenceDiagram
    participant OS as Order Service
    participant IS as Inventory Service
    participant PS as Payment Service

    Note over OS,PS: 正常フロー
    OS->>IS: 1. Create Order
    IS->>PS: 2. Reserve Stock
    PS->>PS: 3. Process Payment
    PS-->>IS: Success
    IS-->>OS: 4. All Success

    Note over OS,PS: 補償フロー(決済失敗時)
    PS--xPS: Payment Failed
    PS-->>IS: Rollback Stock
    IS-->>OS: Cancel Order
// Saga オーケストレーター
class OrderSaga {
  async execute(orderData: CreateOrderData): Promise<Order> {
    const sagaLog: SagaStep[] = [];

    try {
      // Step 1: 注文作成
      const order = await this.orderService.create(orderData);
      sagaLog.push({ service: 'order', action: 'create', data: order });

      // Step 2: 在庫予約
      await this.inventoryService.reserve(order.items);
      sagaLog.push({ service: 'inventory', action: 'reserve', data: order.items });

      // Step 3: 決済処理
      await this.paymentService.process(order.id, order.totalAmount);
      sagaLog.push({ service: 'payment', action: 'process', data: order.id });

      // Step 4: 注文確定
      await this.orderService.confirm(order.id);

      return order;

    } catch (error) {
      // 補償トランザクション(逆順で実行)
      await this.compensate(sagaLog);
      throw error;
    }
  }

  private async compensate(sagaLog: SagaStep[]): Promise<void> {
    for (const step of sagaLog.reverse()) {
      switch (step.service) {
        case 'inventory':
          await this.inventoryService.release(step.data);
          break;
        case 'order':
          await this.orderService.cancel(step.data.id);
          break;
      }
    }
  }
}

観測可能性(Observability)

3つの柱

flowchart TB
    subgraph Observability["Observability - 観測可能性の3つの柱"]
        subgraph Logs["Logs"]
            L1["アプリケーション<br/>のイベント記録"]
            L2["ELK Stack / Loki"]
        end

        subgraph Metrics["Metrics"]
            M1["システムの状態<br/>を数値で表現"]
            M2["Prometheus / Grafana"]
        end

        subgraph Traces["Traces"]
            T1["リクエストの流れ<br/>を追跡"]
            T2["Jaeger / Zipkin"]
        end
    end

分散トレーシング

// OpenTelemetry による分散トレーシング
import { trace, context, SpanKind } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

async function createOrder(req: Request): Promise<Order> {
  return tracer.startActiveSpan(
    'createOrder',
    { kind: SpanKind.SERVER },
    async (span) => {
      try {
        span.setAttribute('user.id', req.userId);

        // 子スパン: ユーザー検証
        const user = await tracer.startActiveSpan('validateUser', async (childSpan) => {
          const result = await userClient.getUser(req.userId);
          childSpan.end();
          return result;
        });

        // 子スパン: 注文保存
        const order = await tracer.startActiveSpan('saveOrder', async (childSpan) => {
          const result = await orderRepository.save(req.orderData);
          childSpan.setAttribute('order.id', result.id);
          childSpan.end();
          return result;
        });

        span.setStatus({ code: SpanStatusCode.OK });
        return order;

      } catch (error) {
        span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
        throw error;
      } finally {
        span.end();
      }
    }
  );
}

マイクロサービス導入の判断基準

導入すべきケース

  • チームが10人以上で、独立した開発が必要
  • 異なる部分で異なるスケーリング要件がある
  • 技術スタックの多様性が求められる
  • 障害の分離が重要

避けるべきケース

  • 小規模なチーム(3-5人程度)
  • ドメインの理解が不十分な初期段階
  • 運用能力(監視、CI/CD)が不足している
  • 単純なCRUDアプリケーション

重要: 「モノリスから始めて、必要に応じて分割する」アプローチが多くの場合で推奨されます。

まとめ

マイクロサービスアーキテクチャは、適切に実装すれば大きなメリットをもたらしますが、複雑性も伴います。

設計原則

  1. 単一責任: 1サービス = 1ビジネス機能
  2. データ独立: サービスごとにDB分離
  3. 疎結合: インターフェースを通じた通信
  4. 高凝集: 関連機能は同一サービス内に

必須パターン

  • API Gateway: 単一エントリーポイント
  • サーキットブレーカー: 障害伝播の防止
  • Saga: 分散トランザクション管理
  • 分散トレーシング: リクエスト追跡

成功するマイクロサービスの鍵は、適切な境界の定義と、堅牢な運用基盤の構築にあります。

参考リンク

← 一覧に戻る