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

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);
 }
 }
}

ポイント: 同期通信(REST/gRPC)は即座にレスポンスが必要な場合に、非同期通信(メッセージキュー)は結果整合性が許容される場合に使い分けます。

通信パターンの選択基準

パターンユースケース特徴
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;
 }
}

注意: サーキットブレーカーを導入しないと、1つのサービスの障害が全体に波及(カスケード障害)する危険があります

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: 分散トランザクション管理
  • 分散トレーシング: リクエスト追跡

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

現場からの教訓

実際のプロジェクトで学んだこと

  1. 最初からマイクロサービスにしない

ECサイトのリプレイスプロジェクトでは、最初にモノリスで構築し、ユーザー数が10万を超えた段階で段階的に分割しました。初期段階での過度な分割は、開発速度を著しく低下させます。

  1. サービス境界の見直しは避けられない

注文サービスと配送サービスを分離した際、後から「配送ステータス更新」機能がどちらに属すべきか問題になりました。ドメインエキスパートと密に連携し、境界を再定義することが重要です。

  1. 分散トレーシングは初日から導入する

障害発生時に「どのサービスで問題が起きたか」を特定するのに数時間かかることがありました。OpenTelemetryなどのトレーシングツールは、サービス数が少ないうちから導入すべきです。

マイクロサービス移行のチェックリスト

  • CI/CDパイプラインが整備されているか
  • コンテナオーケストレーション基盤があるか
  • 集中ログ管理システムがあるか
  • 分散トレーシングの仕組みがあるか
  • サービス間認証の方針が決まっているか
  • データ整合性の許容範囲が定義されているか

実践メモ: マイクロサービス移行のチェックリストを事前に確認し、CI/CD・監視・トレーシングの基盤が整っていることを確認してから移行を開始しましょう。

まとめ

この記事は、名前だけで覚えるより、登場人物、処理の流れ、失敗しやすい点をセットで理解することが重要です。実装で使う前に、公式ドキュメントと実際のリクエストやログを確認してください。

関連記事

マイクロサービスを実践するために、以下の記事も参考にしてください:

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR