ストーリー
佐藤CTOがあなたをホワイトボードの前に呼びました。そこには、自社プロダクトのアーキテクチャ図が描かれています。1つの大きな箱の中に、注文管理、在庫管理、ユーザー管理、決済、通知、レポート — すべてが詰め込まれていました。
佐藤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
成功企業の事例
| 企業 | アプローチ |
|---|---|
| Amazon | 2001年頃のモノリスから段階的にサービスを分離 |
| Netflix | 2008年にモノリスからの移行を開始、7年かけて完了 |
| Shopify | 2016年頃から「モジュラーモノリス」を採用、段階的に分離 |
| 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分