S
Shineos Tech Blog
マイクロサービスかモノリスか?スタートアップのフェーズに応じたアーキテクチャ選定の判断基準

マイクロサービスかモノリスか?スタートアップのフェーズに応じたアーキテクチャ選定の判断基準

| Shineos Dev Team
Share:

はじめに

スタートアップの技術責任者として、「マイクロサービスにすべきか、モノリスで始めるべきか」という質問は避けて通れません。多くの技術記事では「スケーラビリティならマイクロサービス」「初期開発ならモノリス」といった一般論が語られますが、実際の現場では チームの規模、資金調達状況、プロダクトの成長速度 といった複数の変数が絡み合い、単純な二択では判断できないことが大半です。

私たちも過去に、MVP段階でマイクロサービスを選択して開発速度が大幅に低下したケースや、逆にモノリスで急成長し、リファクタリングに多大なコストがかかったケースを経験してきました。この記事では、そうした失敗から学んだ スタートアップの成長フェーズごとの現実的な判断基準 を、具体的な実装パターンとともに解説します。

アーキテクチャ選定とは?

アーキテクチャ選定とは、システム全体の構造を決定するプロセスであり、モノリシックアーキテクチャ(モノリス)マイクロサービスアーキテクチャ の二つが主な選択肢となります。

モノリシックアーキテクチャ は、すべての機能が単一のコードベースにまとまった構造です。デプロイが単純で、開発初期のスピードが速く、トランザクション管理も容易です。一方で、規模が大きくなるとコードの複雑性が増し、部分的な変更でもシステム全体の再デプロイが必要になります。

マイクロサービスアーキテクチャ は、機能ごとに独立したサービスに分割し、それぞれが独自のデータベースを持つ構造です。各サービスを独立して開発・デプロイできるため、大規模チームでの並行開発に向いています。しかし、サービス間通信のオーバーヘッド、分散トランザクションの複雑さ、運用の難易度が高まるというトレードオフがあります。

まとめ

本記事の重要なポイントを以下に整理します。

  • スタートアップのフェーズ(MVP期・成長期・拡大期)ごとに最適なアーキテクチャは異なる
  • MVP期はモノリスで迅速な検証を優先し、成長期はモジュラーモノリスで移行準備、拡大期にマイクロサービス化を検討
  • アーキテクチャ選定は技術的要件だけでなく、チーム規模・資金状況・ビジネス要求を総合的に判断
  • 段階的な移行戦略(Strangler Figパターン)を用いることで、リスクを抑えながらアーキテクチャを進化させられる
  • 一度決めたアーキテクチャは永続的ではなく、プロダクトの成長に応じて見直すことが重要

スタートアップの成長フェーズとアーキテクチャの関係

スタートアップは一般的に「MVP期」「成長期」「拡大期」という3つのフェーズを経て成長します。それぞれのフェーズで、チーム規模、資金状況、技術的要求が大きく異なるため、アーキテクチャの選定基準も変わります。

MVP期:迅速な検証を最優先

MVP(Minimum Viable Product)期の最大の目標は、プロダクトの仮説検証を最速で行うこと です。このフェーズでは、エンジニアリングチームは1〜3名程度、資金はシードラウンド以前またはエンジェル投資のみ、という状況が一般的です。

この段階で マイクロサービスを選択するのは明らかにオーバーエンジニアリング です。サービス間通信、API Gateway、サービスメッシュ、分散トレーシングなどのインフラ構築に時間を取られ、肝心のプロダクト開発が遅れます。

推奨アーキテクチャ:シンプルなモノリス

// MVP期のシンプルなモノリス構成例(Next.js App Router)
// app/api/users/route.ts
import { prisma } from '@/lib/prisma';

export async function POST(request: Request) {
  const { name, email } = await request.json();
  
  // バリデーション、ビジネスロジック、DB操作をすべて一箇所に集約
  const user = await prisma.user.create({
    data: { name, email }
  });
  
  return Response.json({ user });
}

// app/api/orders/route.ts
export async function POST(request: Request) {
  const { userId, productId } = await request.json();
  
  // トランザクション処理も単一DBで完結
  const order = await prisma.$transaction(async (tx) => {
    const product = await tx.product.findUnique({ where: { id: productId } });
    if (!product || product.stock < 1) {
      throw new Error('在庫不足');
    }
    
    await tx.product.update({
      where: { id: productId },
      data: { stock: { decrement: 1 } }
    });
    
    return tx.order.create({
      data: { userId, productId }
    });
  });
  
  return Response.json({ order });
}

この時期は 「動くものを早く作る」 ことが最優先です。コードの美しさやスケーラビリティは二の次で構いません。

成長期:モジュラーモノリスで移行準備

PMF(Product Market Fit)を達成し、ユーザーが急増し始めると、チームは5〜15名程度に拡大し、シリーズAの資金調達が完了している状態になります。この段階では、「機能追加のスピードを保ちながら、将来のスケーラビリティも視野に入れる」というバランスが求められます。

ここで推奨されるのが モジュラーモノリス です。これは、コードベースは単一のまま、内部をドメイン境界(Bounded Context)に沿って明確にモジュール分割する設計です。

モジュラーモノリスの実装例

// src/modules/user/service.ts
export class UserService {
  async createUser(data: CreateUserDTO) {
    // ユーザー作成ロジック
    return await prisma.user.create({ data });
  }
  
  async getUser(id: string) {
    return await prisma.user.findUnique({ where: { id } });
  }
}

// src/modules/order/service.ts
import { UserService } from '../user/service';
import { ProductService } from '../product/service';

export class OrderService {
  constructor(
    private userService: UserService,
    private productService: ProductService
  ) {}
  
  async createOrder(userId: string, productId: string) {
    // 依存関係をインジェクションで受け取る
    const user = await this.userService.getUser(userId);
    const product = await this.productService.getProduct(productId);
    
    if (!user) throw new Error('ユーザーが見つかりません');
    if (!product || product.stock < 1) throw new Error('在庫不足');
    
    return await prisma.order.create({
      data: { userId, productId }
    });
  }
}

モジュラーモノリスの利点は、将来的にマイクロサービスへ分割する際の境界線がすでに引かれている ことです。各モジュールは明確なインターフェースを持ち、他モジュールへの依存が限定的なため、必要になったタイミングで該当モジュールだけを別サービスとして切り出せます。

成長フェーズごとのアーキテクチャ選定基準

拡大期:選択的マイクロサービス化

シリーズB以降の資金調達を完了し、チームが20名以上になると、「複数チームが並行して開発できる体制」が必要になります。この段階で初めて マイクロサービス化を本格的に検討 すべきです。

ただし、すべてをマイクロサービス化する必要はありません。ビジネス上のボトルネックや、スケーラビリティが求められる部分だけを切り出す「選択的マイクロサービス化」が現実的です。

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

以下の条件を 2つ以上 満たす機能は、マイクロサービスとして分離を検討する価値があります。

判断基準説明
負荷が集中している特定の機能だけがCPU/メモリを圧迫し、他機能に影響を与えている
独立したチームが担当専任の開発チームが存在し、独自のリリースサイクルを持つ
技術スタックが異なる機械学習モデルなど、特定の言語/フレームワークが最適な場合
外部連携が多いサードパーティAPIとの通信が中心で、他機能との結合が弱い
データの独立性が高い他機能とのデータ依存が少なく、トランザクション境界が明確

段階的な移行:Strangler Figパターン

既存のモノリスからマイクロサービスへの移行には、Strangler Figパターン が有効です。これは、古い木に巻きつきながら成長する絞め殺しイチジク(Strangler Fig)にちなんだ名前で、既存システムを徐々に新システムに置き換えていく手法です。

// 1. API Gatewayで新旧を振り分け(Next.js Middleware例)
// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request: Request) {
  const url = new URL(request.url);
  
  // 注文APIは新しいマイクロサービスに転送
  if (url.pathname.startsWith('/api/orders')) {
    return NextResponse.rewrite(
      new URL(url.pathname, process.env.ORDER_SERVICE_URL)
    );
  }
  
  // それ以外は既存のモノリスで処理
  return NextResponse.next();
}

// 2. マイクロサービス側の実装(注文サービス)
// services/order-service/src/index.ts
import express from 'express';
import { PrismaClient } from '@prisma/client';

const app = express();
const prisma = new PrismaClient();

app.post('/api/orders', async (req, res) => {
  const { userId, productId } = req.body;
  
  // 他サービスのデータは HTTP API 経由で取得
  const userResponse = await fetch(`${process.env.USER_SERVICE_URL}/api/users/${userId}`);
  const user = await userResponse.json();
  
  const productResponse = await fetch(`${process.env.PRODUCT_SERVICE_URL}/api/products/${productId}`);
  const product = await productResponse.json();
  
  if (!product || product.stock < 1) {
    return res.status(400).json({ error: '在庫不足' });
  }
  
  // 注文データは自分のDBに保存
  const order = await prisma.order.create({
    data: { userId, productId }
  });
  
  res.json({ order });
});

app.listen(3001);

この方法の利点は、リスクを最小限に抑えながら段階的に移行できる ことです。新サービスに問題があれば、ルーティングを戻すだけで元に戻せます。

アーキテクチャ選定の失敗パターンと対策

実際のプロジェクトでよく見られる失敗パターンと、その対策を紹介します。

失敗パターン1:MVP期にマイクロサービスを採用

状況:エンジニア2名のチームで、「将来のスケーラビリティを考えて」最初からマイクロサービスで構築を開始。

結果:認証サービス、ユーザーサービス、商品サービス、注文サービスの4つに分割。それぞれにDocker環境、CI/CD、APIドキュメント、テストを整備する必要があり、最初のMVPリリースまでに6ヶ月を要した。競合が同じ市場に3ヶ月で参入し、機会を逃した。

対策:MVP期は モノリスで最速リリース を優先。インフラの複雑性は後回しにし、プロダクトの仮説検証に集中すべきです。

失敗パターン2:成長期に技術的負債を放置

状況:PMF達成後、機能追加を優先してコードの整理を後回しにした。結果、すべての機能が密結合し、1つの変更が広範囲に影響する状態に。

結果:チームが10名に増えたが、並行開発が困難で開発速度が逆に低下。リファクタリングに3ヶ月を要し、その間新機能開発が完全に停止。

対策:成長期には モジュラーモノリスへのリファクタリング を計画的に実施。週1回の「技術的負債返済デー」を設け、段階的にモジュール境界を明確化します。

失敗パターン3:マイクロサービス化の粒度ミス

状況:マイクロサービス化の際、機能を細かく分割しすぎた(20以上のサービス)。結果、1つの画面表示に10回以上のAPI呼び出しが必要になり、レイテンシが大幅に悪化。

結果:ユーザー体験が著しく低下し、サービスのパフォーマンス問題が頻発。サービス間通信のデバッグに多大な時間を費やすことに。

対策:マイクロサービスの粒度は ビジネスドメイン単位 で決定。DDD(ドメイン駆動設計)のBounded Contextを参考に、適切なサイズで分割します。目安として、1チーム(5〜7名)が管理できる範囲は2〜3サービス程度です。

実装における具体的な判断基準

アーキテクチャ選定を具体的に判断するためのチェックリストを以下に示します。

チーム規模による判断

チーム規模推奨アーキテクチャ理由
1〜3名シンプルなモノリス並行開発の必要性が低く、単純な構成で開発速度を最大化
4〜10名モジュラーモノリス一定の並行開発が必要だが、マイクロサービスの運用コストは高すぎる
11〜20名モジュラーモノリス + 一部マイクロサービスボトルネックとなる機能のみ分離し、運用コストを抑える
21名以上選択的マイクロサービス複数チームの完全な独立開発を実現

資金状況による判断

シードラウンド以前:インフラコストを最小化するため、モノリスで単一のコンテナ/サーバーで稼働。Vercel、Render、Fly.ioなどのPaaSを活用し、インフラ管理を最小化します。

シリーズA以降:成長に伴うインフラ投資が可能になるため、AWSやGCPでKubernetesクラスタを構築し、選択的にマイクロサービス化を進めます。

トランザクション要件による判断

強いトランザクション整合性が必要な場合(例:決済、在庫管理)は、モノリスまたは単一のマイクロサービス内で完結させるべきです。分散トランザクション(Saga パターン、2フェーズコミット)は実装が複雑で、失敗時のリカバリ処理が困難です。

// モノリスでのトランザクション処理(推奨)
async function processPayment(userId: string, amount: number) {
  return await prisma.$transaction(async (tx) => {
    // 残高確認
    const wallet = await tx.wallet.findUnique({ where: { userId } });
    if (wallet.balance < amount) throw new Error('残高不足');
    
    // 残高減算
    await tx.wallet.update({
      where: { userId },
      data: { balance: { decrement: amount } }
    });
    
    // 支払い記録作成
    const payment = await tx.payment.create({
      data: { userId, amount }
    });
    
    return payment;
  });
}

// マイクロサービスでの分散トランザクション(複雑で推奨しない)
async function processPaymentDistributed(userId: string, amount: number) {
  // Sagaパターンで実装が必要
  let compensationSteps = [];
  
  try {
    // Step 1: 残高確認・減算(Wallet Service)
    const walletResult = await fetch(`${WALLET_SERVICE}/deduct`, {
      method: 'POST',
      body: JSON.stringify({ userId, amount })
    });
    compensationSteps.push(() => 
      fetch(`${WALLET_SERVICE}/refund`, { 
        method: 'POST', 
        body: JSON.stringify({ userId, amount }) 
      })
    );
    
    // Step 2: 支払い記録(Payment Service)
    const paymentResult = await fetch(`${PAYMENT_SERVICE}/record`, {
      method: 'POST',
      body: JSON.stringify({ userId, amount })
    });
    
    return paymentResult;
  } catch (error) {
    // エラー時は補償トランザクション実行
    for (const compensate of compensationSteps.reverse()) {
      await compensate();
    }
    throw error;
  }
}

運用面での考慮事項

アーキテクチャ選定は開発だけでなく、運用の容易性 も重要な判断基準です。

モニタリング・ログ管理

モノリス:単一のアプリケーションログを追跡すればよく、エラーの原因特定が容易です。

マイクロサービス:分散トレーシング(Jaeger、OpenTelemetry)、集約ログ(ELK Stack、Datadog)の整備が必須です。これらのツールの導入と運用には、専任のSREまたはDevOpsエンジニアが必要になります。

デプロイ戦略

モノリス:Blue-Green デプロイやカナリアリリースをアプリケーション全体で実施。一度に全機能をリリースするため、テストの負担は大きいですが、手順は単純です。

マイクロサービス:各サービスを独立してデプロイできるため、リリース頻度を上げやすい反面、サービス間のバージョン互換性管理が必要です。API仕様の後方互換性を保つため、APIバージョニング(/v1/users/v2/users)を導入します。

よくある質問

既存のモノリスをマイクロサービスに移行する際、どこから始めるべきですか?

回答:まず「ボトルネックの特定」から始めます。New RelicやDatadogなどのAPMツールで、最も負荷の高いエンドポイント、レスポンスタイムが長い処理を可視化します。その中で、他機能との依存が少なく、明確な境界を持つ機能 から分離するのが成功しやすいパターンです。例えば、画像処理サービス、通知サービス、レポート生成サービスなどは、分離しやすい典型例です。

マイクロサービスにすると本当にスケールしやすくなりますか?

回答:「スケールしやすくなる」というのは半分正解で半分誤解です。マイクロサービスは 部分的なスケール を可能にします。例えば、検索サービスだけにトラフィックが集中している場合、そのサービスのインスタンスだけを増やせます。しかし、サービス間通信のオーバーヘッド、データの整合性管理、分散システム特有の障害パターン(ネットワーク分断、タイムアウト)などの複雑性が増すため、運用コストは確実に上がります。トレードオフを理解した上で判断してください。

「モジュラーモノリス」と「マイクロサービス」の境界はどこですか?

回答:最大の違いは デプロイの独立性データベースの分離 です。モジュラーモノリスは、コードをモジュール分割しても単一のアプリケーションとしてデプロイし、データベースも共有します。一方、マイクロサービスは各サービスが独立してデプロイされ、それぞれが専用のデータベースを持ちます(Database per Service パターン)。組織としてチームごとに独立したリリースサイクルが必要になったタイミングが、マイクロサービス化を検討すべき境界線と言えます。

Kubernetesを導入すべきタイミングはいつですか?

回答:Kubernetesは強力なオーケストレーションツールですが、学習コストと運用負荷が非常に高いため、20名以上のチームで複数のマイクロサービスを運用する段階 まで導入を遅らせることを推奨します。MVP期や成長期では、Vercel、Render、Fly.io、AWS App Runnerなどのマネージドプラットフォームで十分です。「Kubernetesがないとスケールできない」というのは誤解で、適切なアーキテクチャとキャッシュ戦略があれば、PaaSでも十分に成長できます。

おわりに

アーキテクチャ選定は、技術的な美しさではなく ビジネスの成長とチームの現実に合わせた実用的な判断 が求められます。MVP期にはシンプルなモノリスで仮説検証を最優先し、成長期にはモジュラーモノリスで柔軟性を確保し、拡大期に初めて選択的なマイクロサービス化を検討する、という段階的なアプローチが、私たちの経験では最も成功率が高い戦略です。

「いつかスケールするかもしれない」という不安から過剰に設計するのではなく、今必要なものを、今のチームで実現できる範囲で構築する ことが、スタートアップにおける技術的負債を最小化する最良の方法です。

私たちShineosでは、スタートアップのフェーズに応じたアーキテクチャ設計支援を行っています。MVP構築から成長期のリファクタリング、拡大期のマイクロサービス移行まで、実践的なサポートを提供しています。ご興味のある方は、ぜひお気軽にご相談ください。

参考リンク