LESSON 30分

ストーリー

佐藤CTOがあなたをホワイトボードの前に呼びました。そこには、自社プロダクトのアーキテクチャ図が描かれています。1つの大きな箱の中に、注文管理、在庫管理、ユーザー管理、決済、通知、レポート — すべてが詰め込まれていました。

佐藤CTO
このモノリスは創業以来5年間、僕たちを支えてきた。最初は本当にうまく機能していた。少人数のチームで素早く開発できたし、デプロイも簡単だった
あなた
でも最近は…
佐藤CTO
そう。デプロイに2時間かかる。注文機能の変更が在庫管理を壊す。新しいメンバーが入っても、コードベースが大きすぎて立ち上がりに2ヶ月かかる。これがモノリスの”影”だ

佐藤CTOはペンを持ち替えました。

佐藤CTO
だからといって、モノリスが悪だと言いたいわけじゃない。光と影の両方を正確に理解することが、正しい移行判断の第一歩だ

モノリスアーキテクチャとは

モノリスアーキテクチャとは、アプリケーションのすべての機能が単一のデプロイ単位として構築・実行される設計パターンです。

// 典型的なモノリスの構成
// すべてが1つのプロジェクト内に存在する
src/
├── controllers/
│   ├── OrderController.ts      // 注文API
│   ├── UserController.ts       // ユーザーAPI
│   ├── InventoryController.ts  // 在庫API
│   └── PaymentController.ts    // 決済API
├── services/
│   ├── OrderService.ts
│   ├── UserService.ts
│   ├── InventoryService.ts
│   └── PaymentService.ts
├── repositories/
│   ├── OrderRepository.ts
│   ├── UserRepository.ts
│   └── InventoryRepository.ts
├── models/
│   ├── Order.ts
│   ├── User.ts
│   └── Product.ts
└── database/
    └── schema.sql  // 1つの共有データベース

モノリスの光:5つの利点

1. 開発のシンプルさ

モノリスは、特に初期段階で圧倒的にシンプルです。

// モノリスでは関数呼び出しだけで済む
class OrderService {
  constructor(
    private inventoryService: InventoryService,
    private paymentService: PaymentService,
    private notificationService: NotificationService,
  ) {}

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    // 在庫確認 -- ただの関数呼び出し
    const available = await this.inventoryService.checkStock(dto.items);
    if (!available) throw new InsufficientStockError();

    // 決済処理 -- ただの関数呼び出し
    const payment = await this.paymentService.charge(dto.paymentMethod, dto.total);

    // 注文作成
    const order = await this.orderRepo.save(new Order(dto, payment));

    // 通知 -- ただの関数呼び出し
    await this.notificationService.sendOrderConfirmation(order);

    return order;
  }
}

マイクロサービスなら、これらの各呼び出しがネットワーク越しのAPI呼び出しになり、タイムアウト、リトライ、サーキットブレーカーを考慮する必要があります。

2. デプロイの容易さ

観点モノリスマイクロサービス
デプロイ単位1つのアーティファクトサービスごとに個別
デプロイ手順ビルド→テスト→デプロイサービスごとのCI/CDパイプライン
環境構築1つの環境サービス数 x 環境数
バージョン管理1つのバージョンサービス間の互換性管理

3. データの一貫性

// モノリスならトランザクションで一貫性を簡単に保証できる
async transferOrder(orderId: string, newWarehouseId: string): Promise<void> {
  await this.db.transaction(async (tx) => {
    // 元の倉庫の在庫を減らす
    await tx.inventory.decrement(orderId, currentWarehouseId);
    // 新しい倉庫の在庫を増やす
    await tx.inventory.increment(orderId, newWarehouseId);
    // 注文の倉庫を更新
    await tx.orders.updateWarehouse(orderId, newWarehouseId);
    // すべて成功するか、すべて失敗するか
  });
}

マイクロサービスでは、分散トランザクション(Sagaパターンなど)が必要になり、複雑さが桁違いに増します。

4. テストの容易さ

// モノリスではEnd-to-Endテストが比較的簡単
describe('注文フロー', () => {
  it('在庫あり→決済成功→注文完了', async () => {
    // 1つのアプリケーション内で完結するテスト
    const user = await createTestUser();
    const product = await createTestProduct({ stock: 10 });

    const order = await request(app)
      .post('/api/orders')
      .send({ userId: user.id, items: [{ productId: product.id, quantity: 2 }] })
      .expect(201);

    // DB直接確認で在庫が減っていることを検証
    const updatedProduct = await productRepo.findById(product.id);
    expect(updatedProduct.stock).toBe(8);
  });
});

5. 運用のシンプルさ

運用タスクモノリスマイクロサービス
監視1アプリの監視数十~数百サービスの監視
ログ1箇所に集約分散トレーシングが必要
デバッグスタックトレースで追跡サービス間のリクエスト追跡
インフラコストサーバー数が少ないサービスごとのインフラ

モノリスの影:成長に伴う痛み

1. デプロイのボトルネック

graph LR
    A["チームA:<br/>注文機能の改善"]
    B["チームB:<br/>在庫管理のバグ修正"]
    C["チームC:<br/>新しい決済手段追加"]
    R["1つのリリースにまとめる"]
    D["デプロイ"]

    A --> R
    B --> R
    C --> R
    R --> D
問題:
- チームBのバグ修正が急ぎでも、チームAの作業を待つ必要がある
- チームCの変更がテストで失敗すると、全体のリリースが止まる
- デプロイ頻度が週1回→月2回に低下
// 実際の影響を数値で見る
interface DeploymentMetrics {
  // 初期(チーム3名)
  early: {
    deployFrequency: "1日数回";
    leadTime: "数時間";
    buildTime: "2分";
    testTime: "5分";
  };
  // 成長後(チーム30名)
  grown: {
    deployFrequency: "月2回";
    leadTime: "2-3週間";
    buildTime: "30分";
    testTime: "2時間";
  };
}

2. スケーリングの制約

graph TD
    subgraph モノリスのスケーリング
        M1["注文 10% | 在庫 80% | 決済 5% | ユーザー 5%<br/>← 全体をスケール"]
        M2["× 5台(在庫の負荷に合わせてスケール)"]
        M1 --- M2
    end

    subgraph マイクロサービスのスケーリング
        S1["注文 ×1"]
        S2["在庫 ×5"]
        S3["決済 ×1"]
        S4["ユーザー ×1"]
    end

    style M1 fill:#ffebee,stroke:#c62828
    style S1 fill:#e8f5e9,stroke:#2e7d32
    style S2 fill:#e8f5e9,stroke:#2e7d32
    style S3 fill:#e8f5e9,stroke:#2e7d32
    style S4 fill:#e8f5e9,stroke:#2e7d32

3. チームの結合

// チームAが変更した共有モデル
interface Product {
  id: string;
  name: string;
  price: number;
  // チームAが追加: カテゴリ情報
  category: ProductCategory;  // ← この変更が他チームに波及
  // チームBが追加: 在庫情報
  stockLevel: number;
  // チームCが追加: レビュー情報
  averageRating: number;
}
// 全チームが同じモデルを共有しているため、
// 1つの変更が予期しない影響を引き起こす

4. 技術的ロックイン

制約具体例
言語固定全体がTypeScriptなので、ML機能にPythonを使えない
フレームワーク固定Express v3から移行したいが、全体に影響する
DB固定PostgreSQLだが、検索機能にはElasticsearchが最適
ランタイム固定Node.js 16だが、新しいAPIはNode.js 20が必要

5. 認知負荷の増大

xychart-beta
    title "コードベースの成長と認知負荷"
    x-axis ["10万行", "50万行", "100万行", "200万行"]
    y-axis "認知負荷" 0 --> 100
    line [10, 35, 65, 95]
  10万行: 新メンバーが1週間で生産的になれる
  50万行: 新メンバーが1ヶ月で一部の領域に貢献できる
 200万行: 新メンバーが2-3ヶ月かけても全体像を掴めない

モノリスが正しい選択であるケース

モノリスは「悪」ではありません。以下の状況ではモノリスが最適です。

状況理由
スタートアップ初期ビジネスモデルが固まっていない段階で分割するのは時期尚早
チームが小さい(10名以下)マイクロサービスの運用コストがメリットを上回る
ドメインが明確でない間違った境界で分割すると、後の修正コストが膨大
シンプルなアプリケーション複雑さが低いなら、モノリスのシンプルさが勝る
厳密なトランザクションが必要金融系など、分散トランザクションのリスクが高い場合

「Monolith First」アプローチ

Martin Fowler が提唱するこのアプローチは、多くの成功企業が実践しています。

graph TD
    subgraph "Phase 1: モノリスで始める"
        direction LR
        subgraph WM["Well-Structured Monolith<br/>明確なモジュール境界"]
            OM1["注文<br/>モジュール"]
            IM1["在庫<br/>モジュール"]
            PM1["決済<br/>モジュール"]
        end
    end

    subgraph "Phase 2: 成長に応じて段階的に抽出"
        direction LR
        subgraph MO["Monolith"]
            OM2["注文"]
            PM2["決済"]
        end
        IS["在庫<br/>サービス"]
    end

    WM -->|"ボトルネックの<br/>モジュールから抽出"| MO
    WM -->|"ボトルネックの<br/>モジュールから抽出"| IS

    style IS fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

成功企業の事例

企業アプローチ
Amazon2001年頃のモノリスから段階的にサービスを分離
Netflix2008年にモノリスからの移行を開始、7年かけて完了
Shopify2016年頃から「モジュラーモノリス」を採用、段階的に分離
Basecamp意図的にモノリスを維持。小チームにはモノリスが最適と判断
// Monolith First の実践:モジュール境界を明確にする
// modules/order/index.ts -- 公開APIだけをexport
export { OrderService } from './services/OrderService';
export { CreateOrderDto } from './dto/CreateOrderDto';
export { Order } from './entities/Order';
// 内部の実装は非公開

// modules/order/services/OrderService.ts
class OrderService {
  // 他モジュールへの依存はインターフェース経由
  constructor(
    private inventoryPort: InventoryPort,  // 在庫モジュールへのポート
    private paymentPort: PaymentPort,      // 決済モジュールへのポート
  ) {}
}

// このモジュール境界が、将来のサービス境界になる

モノリスの成熟度診断

自社のモノリスがどの段階にあるかを判断するための指標です。

段階特徴推奨アクション
健全期デプロイが高速、変更が容易そのまま維持
注意期ビルド時間増加、一部テスト不安定モジュール境界の強化
警告期デプロイ頻度低下、障害が波及移行計画の策定を開始
危機期変更が怖い、開発速度が大幅低下段階的な移行を実行
// 定量的な診断指標
interface MonolithHealthMetrics {
  deployFrequency: number;       // 週あたりのデプロイ回数
  leadTime: number;              // コミットからデプロイまでの日数
  changeFailureRate: number;     // デプロイ後の障害発生率
  mttr: number;                  // 平均復旧時間(時間)
  buildTime: number;             // ビルド時間(分)
  testExecutionTime: number;     // テスト実行時間(分)
  teamCouplingIndex: number;     // チーム間の変更依存度(0-1)
  newMemberRampUpDays: number;   // 新メンバーの立ち上がり日数
}

function diagnoseMonolith(metrics: MonolithHealthMetrics): HealthStage {
  if (metrics.deployFrequency >= 5 && metrics.leadTime <= 1) return 'healthy';
  if (metrics.deployFrequency >= 2 && metrics.leadTime <= 7) return 'caution';
  if (metrics.deployFrequency >= 0.5 && metrics.leadTime <= 14) return 'warning';
  return 'critical';
}

まとめ

ポイント内容
モノリスの利点シンプルさ、デプロイ容易性、データ一貫性、テスト容易性、運用シンプルさ
モノリスの課題デプロイボトルネック、スケーリング制約、チーム結合、技術的ロックイン
Monolith Firstまずモノリスで始め、成長に応じて段階的に分離する
重要な判断基準チームサイズ、ドメインの明確さ、スケーリング要件
正しいアプローチ光と影の両方を理解した上で、状況に応じた判断をする

チェックリスト

  • モノリスの5つの利点を説明できる
  • モノリスの成長に伴う5つの痛みを理解した
  • 「Monolith First」アプローチの意義を理解した
  • 自社のモノリスの成熟度を診断する観点を持った

次のステップへ

次は「技術的負債の可視化」を学びます。モノリスの影を具体的に数値化し、チームや経営陣に移行の必要性を説得力を持って伝える方法を身につけましょう。


推定読了時間: 30分