システムアーキテクチャの基本パターン
システム設計における基本的なアーキテクチャパターンを分かりやすく解説。レイヤードアーキテクチャ、マイクロサービス、MVC、イベント駆動など、実践的なパターンとその適用場面を学びます。
🎯 この記事で学べること
- 1システムアーキテクチャの重要性と設計原則を理解できます
- 2レイヤードアーキテクチャとその実装方法を習得できます
- 3MVCやMVVMなどのアーキテクチャパターンを学べます
- 4マイクロサービスとモノリスの使い分けを理解できます
- 5イベント駆動アーキテクチャとリアクティブシステムの概念を把握できます
読了時間: 約5分
Twitterが3回死んだ理由
2008年、Twitter は「Fail Whale」で有名だった。
巨大なクジラが小さな鳥たちに持ち上げられている画像。このエラー画面を見たことがある人は多いだろう。当時、Twitter は頻繁にダウンしていた。オバマ大統領の当選、マイケル・ジャクソンの訃報、ワールドカップ。大きなイベントのたびに、サービスが落ちた。
なぜか?すべてが一つの巨大な Ruby on Rails アプリケーションに詰め込まれていたからだ。ツイート投稿、タイムライン生成、通知、検索。あらゆる機能が密結合し、一つの問題が全体を停止させた。
エンジニアのエヴァン・ウィーバーは振り返る。「まるで心臓手術をしながら、マラソンを走っているようだった」。
Twitter は3回、アーキテクチャを根本から作り直した。
第1世代:モノリシックな Rails アプリケーション。シンプルだが、スケールしない。
第2世代:サービス指向アーキテクチャ。機能を分離したが、依然として同期的で遅い。
第3世代:イベント駆動マイクロサービス。各サービスが独立し、非同期で通信する。
この進化の過程で、Twitter は貴重な教訓を学んだ。アーキテクチャは、単なる技術的な選択ではない。それは、ビジネスの成長を支えるか、妨げるかを決定する戦略的な意思決定だ。
eBayの10億ドルの決断
1995年、ピエール・オミダイアは自宅でオークションサイトを立ち上げた。
最初のアーキテクチャは、Perl で書かれた CGI スクリプト。商品リスト、入札、決済、すべてが一つのファイルに収まっていた。最初の取引は、壊れたレーザーポインターで14.83ドル。
10年後、eBay は1日10億ページビューを処理していた。しかし、システムは限界に達していた。2日間の完全停止。損失額は500万ドル。株価は25%下落。
CTO のマイク・フィッシャーは決断を迫られた。「パッチワークを続けるか、ゼロから作り直すか」。
彼が選んだのは、当時としては革新的なアプローチだった。システムを縦に切るのではなく、横に切る。つまり、機能別ではなく、レイヤー別に分割する。
旧アーキテクチャ: 新アーキテクチャ:
┌─────────────┐ ┌─────────────────────────┐
│ 商品機能 │ │ プレゼンテーション層 │
│ 入札機能 │ ═══════> ├─────────────────────────┤
│ 決済機能 │ │ ビジネスロジック層 │
│ 検索機能 │ ├─────────────────────────┤
└─────────────┘ │ データアクセス層 │
└─────────────────────────┘
この決断は10億ドルの価値があった。新アーキテクチャにより、開発速度は3倍に。バグは80%減少。そして何より、チームを機能別ではなくレイヤー別に編成できるようになった。フロントエンドチーム、バックエンドチーム、データベースチーム。それぞれが専門性を発揮できる。
建築家と料理人の共通点
フランク・ロイド・ライトは言った。「建築とは、凍った音楽である」。
優れた建築には、パターンがある。日本の寺院の五重塔は、なぜ地震で倒れないのか?各層が独立して揺れ、エネルギーを吸収するからだ。ヨーロッパの大聖堂は、なぜ何世紀も持つのか?アーチ構造が重量を分散するからだ。
ソフトウェアアーキテクチャも同じだ。問題に対する解決策のパターンが存在する。
世界的なシェフ、ジョエル・ロブションは語る。「料理にも建築がある」。前菜、メイン、デザート。この構造は、味覚の旅を演出する。甘味の後に塩味は合わない。脂っこい料理の後には、さっぱりしたものを。
システムアーキテクチャも、データの流れ、処理の順序、責任の配置を設計する。良いアーキテクチャは、自然な流れを生む。悪いアーキテクチャは、混乱と苦痛をもたらす。
アーキテクチャパターンは「正解」ではなく「選択肢」です。状況に応じて適切なパターンを選び、時には組み合わせることが重要です。
レイヤードアーキテクチャ:高層ビルの教え
1885年、シカゴで世界初の高層ビル「ホーム・インシュアランス・ビル」が建設された。
10階建て。当時としては驚異的な高さだった。これを可能にしたのは、鉄骨フレーム構造。各階が独立した層として機能し、上の階の重量を下に伝える。もし1階と10階が直接つながっていたら?建物は崩壊する。
ソフトウェアのレイヤードアーキテクチャも同じ原理だ。
// 悪い例:すべてが絡み合っている
app.post('/api/order', async (req, res) => {
// UIの検証
if (!req.body.email || !req.body.items) {
return res.status(400).send('必須項目が不足');
}
// ビジネスロジック
let total = 0;
for (const item of req.body.items) {
total += item.price * item.quantity;
}
// 直接データベースに書き込み
const connection = mysql.createConnection({...});
const result = await connection.query(
'INSERT INTO orders (email, total) VALUES (?, ?)',
[req.body.email, total]
);
// メール送信
await sendgrid.send({
to: req.body.email,
subject: '注文確認',
text: `注文番号: ${result.insertId}`
});
res.json({ orderId: result.insertId });
});
このコードの問題は何か?すべてが一箇所に詰め込まれている。テストは?不可能だ。データベースとメールサービスが必要。ビジネスロジックの再利用?できない。すべてがAPIエンドポイントに縛られている。
レイヤードアーキテクチャで書き直すとこうなる:
// プレゼンテーション層(Controller)
class OrderController {
constructor(orderService) {
this.orderService = orderService;
}
async createOrder(req, res) {
try {
const orderData = this.validateRequest(req.body);
const order = await this.orderService.createOrder(orderData);
res.status(201).json({ orderId: order.id });
} catch (error) {
this.handleError(error, res);
}
}
validateRequest(body) {
if (!body.email || !body.items) {
throw new ValidationError('必須項目が不足しています');
}
return { email: body.email, items: body.items };
}
}
// ビジネスロジック層(Service)
class OrderService {
constructor(orderRepository, emailService, inventoryService) {
this.orderRepository = orderRepository;
this.emailService = emailService;
this.inventoryService = inventoryService;
}
async createOrder(orderData) {
// ビジネスルールの適用
const total = this.calculateTotal(orderData.items);
// 在庫確認
await this.inventoryService.checkAvailability(orderData.items);
// 注文作成
const order = await this.orderRepository.create({
email: orderData.email,
items: orderData.items,
total: total,
status: 'pending'
});
// 非同期でメール送信
this.emailService.sendOrderConfirmation(order)
.catch(error => console.error('メール送信エラー:', error));
return order;
}
calculateTotal(items) {
return items.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
}
}
// データアクセス層(Repository)
class OrderRepository {
constructor(database) {
this.db = database;
}
async create(orderData) {
const query = `
INSERT INTO orders (email, total, status, created_at)
VALUES (?, ?, ?, NOW())
`;
const result = await this.db.execute(query, [
orderData.email,
orderData.total,
orderData.status
]);
return {
id: result.insertId,
...orderData
};
}
}
各層は独立してテスト可能。ビジネスロジックはデータベースを知らない。コントローラーはビジネスルールを知らない。まるで高層ビルの各階のように、それぞれが自分の責任だけを持つ。
MVC:劇場の三役
1979年、ゼロックスのパロアルト研究所。
トリグヴェ・リーンスカウグは、Smalltalk-80 のための新しい設計パターンを考案した。彼のインスピレーションは、意外にも劇場から来ていた。
劇場には3つの要素がある。脚本(Model)、役者(View)、演出家(Controller)。脚本は物語そのもの。役者は観客に見せる。演出家は両者をつなぐ。
// Model:脚本(データと物語)
class TodoModel {
constructor() {
this.todos = [];
this.observers = [];
}
addTodo(text) {
const todo = {
id: Date.now(),
text,
completed: false
};
this.todos.push(todo);
this.notifyObservers();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.notifyObservers();
}
}
// Observerパターンで変更を通知
subscribe(observer) {
this.observers.push(observer);
}
notifyObservers() {
this.observers.forEach(observer => observer.update(this.todos));
}
}
// View:役者(表示)
class TodoView {
constructor(controller) {
this.controller = controller;
this.app = document.getElementById('app');
this.render();
}
render() {
this.app.innerHTML = `
<h1>Todoリスト</h1>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="新しいタスク">
<button type="submit">追加</button>
</form>
<ul id="todo-list"></ul>
`;
this.attachEventListeners();
}
attachEventListeners() {
const form = document.getElementById('todo-form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('todo-input');
this.controller.handleAddTodo(input.value);
input.value = '';
});
}
update(todos) {
const list = document.getElementById('todo-list');
list.innerHTML = todos.map(todo => `
<li class="${todo.completed ? 'completed' : ''}">
<input type="checkbox"
${todo.completed ? 'checked' : ''}
onchange="app.controller.handleToggle(${todo.id})">
${todo.text}
</li>
`).join('');
}
}
// Controller:演出家(制御)
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
// ViewをModelの観察者として登録
this.model.subscribe(this.view);
}
handleAddTodo(text) {
if (text.trim()) {
this.model.addTodo(text);
}
}
handleToggle(id) {
this.model.toggleTodo(id);
}
}
// 劇場の開幕
const model = new TodoModel();
const controller = new TodoController(model, null);
const view = new TodoView(controller);
controller.view = view;
// グローバルに公開(デモ用)
window.app = { controller };
MVCの美しさは、関心の分離にある。Modelはデータの真実を保持する。Viewは表示だけを担当する。Controllerは交通整理をする。
この分離により、同じModelを異なるViewで表示できる。Webアプリ、モバイルアプリ、CLIツール。脚本が同じなら、どんな役者でも演じられる。
Uber:数千のマイクロサービスという街
2014年、Uber のエンジニアは悲鳴を上げていた。
モノリシックなPythonアプリケーションが限界に達していた。新機能の追加に3ヶ月。バグ修正が別のバグを生む。デプロイは深夜の祈りの儀式と化していた。
マット・ランティエリ(当時のチーフアーキテクト)は大胆な提案をした。「都市計画の発想でシステムを再設計しよう」。
都市は、独立した建物の集合体だ。病院が火事になっても、学校は機能する。道路工事があっても、地下鉄は動く。Uber のシステムも、独立したサービスの集合体にすればいい。
// 配車サービス
class RideService {
async requestRide(userId, pickup, destination) {
// 1. ユーザー確認
const user = await this.callService('user-service', `/users/${userId}`);
if (!user.verified) {
throw new Error('ユーザー確認が必要です');
}
// 2. 料金計算
const fare = await this.callService('pricing-service', '/calculate', {
method: 'POST',
body: { pickup, destination, time: new Date() }
});
// 3. 近くのドライバー検索
const drivers = await this.callService('driver-location-service', '/nearby', {
method: 'POST',
body: { location: pickup, radius: 5000 }
});
if (drivers.length === 0) {
throw new Error('利用可能なドライバーがいません');
}
// 4. マッチング
const match = await this.callService('matching-service', '/match', {
method: 'POST',
body: { user, drivers, fare }
});
// 5. 通知
this.fireAndForget('notification-service', '/notify', {
driverId: match.driverId,
message: '新しい配車リクエストがあります'
});
return { rideId: match.rideId, driver: match.driver, fare };
}
async callService(serviceName, path, options = {}) {
// サービスディスカバリー
const serviceUrl = await this.discoverService(serviceName);
// サーキットブレーカー
return this.circuitBreaker.call(serviceName, async () => {
const response = await fetch(`${serviceUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Trace-Id': this.generateTraceId()
}
});
if (!response.ok) {
throw new Error(`Service ${serviceName} returned ${response.status}`);
}
return response.json();
});
}
}
各サービスは独立したチームが所有する。価格設定チームは、配車のロジックを知る必要がない。ドライバー位置チームは、通知の仕組みを知る必要がない。
しかし、マイクロサービスは銀の弾丸ではなかった。
2016年、Uber は2,000以上のマイクロサービスを抱えていた。新しい問題が生まれた。サービス間の依存関係が複雑すぎて、誰も全体像を把握できない。一つのリクエストが50以上のサービスを経由する。レイテンシが積み重なり、応答時間が遅くなる。
そこで Uber は「ドメイン指向マイクロサービス」を導入した。関連するサービスをドメインごとにグループ化。配車ドメイン、決済ドメイン、ドライバー管理ドメイン。都市の中に地区を作るように。
Slack:1秒間に100万イベント
2013年、Slack は小さなゲーム会社の内部ツールだった。
しかし、急速に成長し、2019年には1日のアクティブユーザーが1,200万人を超えた。最大の課題は、リアルタイム性だった。誰かがメッセージを送信したら、チームの全員に即座に届かなければならない。
従来のリクエスト・レスポンス型では無理だった。ポーリング?サーバーが死ぬ。WebSocket?接続管理が複雑すぎる。
解決策は、イベント駆動アーキテクチャだった。
// イベントバス
class EventBus {
constructor() {
this.subscribers = new Map();
this.eventQueue = [];
this.processing = false;
}
// イベント発行
async emit(eventType, data) {
const event = {
id: this.generateEventId(),
type: eventType,
data,
timestamp: new Date(),
version: 1
};
this.eventQueue.push(event);
if (!this.processing) {
await this.processQueue();
}
}
// イベント購読
subscribe(eventType, handler) {
if (!this.subscribers.has(eventType)) {
this.subscribers.set(eventType, new Set());
}
this.subscribers.get(eventType).add(handler);
}
async processQueue() {
this.processing = true;
while (this.eventQueue.length > 0) {
const event = this.eventQueue.shift();
const handlers = this.subscribers.get(event.type) || new Set();
// 並行処理で高速化
await Promise.all(
Array.from(handlers).map(handler =>
this.safeExecute(handler, event)
)
);
}
this.processing = false;
}
async safeExecute(handler, event) {
try {
await handler(event);
} catch (error) {
console.error(`Handler error for ${event.type}:`, error);
// デッドレターキューに送信
this.sendToDeadLetter(event, error);
}
}
}
// メッセージサービス
class MessageService {
constructor(eventBus) {
this.eventBus = eventBus;
}
async sendMessage(channelId, userId, text) {
const message = {
id: this.generateId(),
channelId,
userId,
text,
timestamp: new Date()
};
// データベースに保存
await this.saveToDatabase(message);
// イベント発行(非同期処理のトリガー)
await this.eventBus.emit('message.sent', message);
return message;
}
}
// 通知サービス
class NotificationService {
constructor(eventBus) {
// メッセージ送信イベントを購読
eventBus.subscribe('message.sent', this.handleMessage.bind(this));
}
async handleMessage(event) {
const message = event.data;
// チャンネルメンバーを取得
const members = await this.getChannelMembers(message.channelId);
// 送信者以外に通知
const recipients = members.filter(m => m.id !== message.userId);
// WebSocket経由でリアルタイム配信
await Promise.all(
recipients.map(recipient =>
this.sendWebSocketMessage(recipient.id, {
type: 'new_message',
message
})
)
);
}
}
// 検索インデックスサービス
class SearchIndexService {
constructor(eventBus) {
eventBus.subscribe('message.sent', this.indexMessage.bind(this));
eventBus.subscribe('message.updated', this.updateIndex.bind(this));
eventBus.subscribe('message.deleted', this.removeFromIndex.bind(this));
}
async indexMessage(event) {
const message = event.data;
// Elasticsearchにインデックス
await this.elasticsearch.index({
index: 'messages',
id: message.id,
body: {
channelId: message.channelId,
userId: message.userId,
text: message.text,
timestamp: message.timestamp
}
});
}
}
// 分析サービス
class AnalyticsService {
constructor(eventBus) {
// 複数のイベントを購読
eventBus.subscribe('message.sent', this.trackMessage.bind(this));
eventBus.subscribe('user.joined', this.trackUserJoin.bind(this));
eventBus.subscribe('file.uploaded', this.trackFileUpload.bind(this));
}
async trackMessage(event) {
await this.incrementCounter('messages_sent');
await this.recordMetric('message_length', event.data.text.length);
}
}
イベント駆動の美しさは、疎結合にある。メッセージサービスは、誰がそのイベントを購読しているか知らない。新しい機能(例:メッセージ翻訳サービス)を追加するのは簡単だ。既存のコードに触れることなく、新しいイベントハンドラーを追加するだけ。
しかし、イベント駆動にも課題がある。イベントの順序保証、重複処理、デバッグの困難さ。Slack はこれらの問題に対して、イベントソーシング、冪等性キー、分散トレーシングなどの技術を駆使して対処している。
アーキテクチャは進化する
2007年、Netflix はDVDレンタル企業だった。アーキテクチャ?単純なモノリシックアプリケーション。
2020年、Netflix は世界最大のストリーミングサービスになった。アーキテクチャ?1,000を超えるマイクロサービス。
この間、Netflix のアーキテクチャは7回の大きな進化を遂げた。
第1世代(2000-2007):モノリシックJavaアプリケーション 第2世代(2007-2008):垂直分割(機能別モノリス) 第3世代(2008-2010):サービス指向アーキテクチャ 第4世代(2010-2012):初期マイクロサービス 第5世代(2012-2015):完全マイクロサービス化 第6世代(2015-2018):サーバーレスの導入 第7世代(2018-現在):エッジコンピューティング統合
エイドリアン・コックロフト(元Netflix クラウドアーキテクト)は語る。「アーキテクチャに完成はない。ビジネスが成長すれば、アーキテクチャも成長する必要がある」。
重要なのは、各段階で最適な選択をすることだ。スタートアップがいきなりマイクロサービスを採用するのは、3人家族が100部屋の豪邸に住むようなものだ。逆に、巨大企業がモノリスに固執するのは、1000人を1部屋に詰め込むようなものだ。
パターンを選ぶ技術
アマゾンの元CTOワーナー・ボーゲルスは言った。「すべてはトレードオフだ」。
完璧なアーキテクチャは存在しない。状況に応じて、適切なパターンを選ぶ必要がある。
小規模プロジェクト(1-5人) シンプルなMVCで十分。複雑さは敵だ。市場投入速度が最優先。Instagram は最初、たった2人のエンジニアで作られた。シンプルな Django アプリケーション。それで十分だった。
中規模プロジェクト(10-50人) レイヤードアーキテクチャが効果的。チームを層ごとに分けられる。責任が明確。Airbnb は長い間、この規模で成功していた。Ruby on Rails のモノリスを、きれいにレイヤー分けして運用。
大規模プロジェクト(100人以上) マイクロサービスの出番。ただし、段階的に移行すること。Amazon は「Two Pizza Rule」を採用した。2枚のピザで満腹になる人数(6-8人)で、一つのサービスを担当する。
リアルタイム要求が高い イベント駆動アーキテクチャ。LinkedIn のリアルタイムフィード、Uber のドライバー追跡、オンラインゲームのマルチプレイヤー機能。すべてイベント駆動で実現されている。
「最新の技術を使いたい」という理由でアーキテクチャを選んではいけません。ビジネスの要求、チームの能力、将来の成長を考慮して選択しましょう。
失敗から学ぶ:Friendster の教訓
2003年、Friendster は世界初の本格的なSNSだった。
しかし、2009年にはサービスを停止した。技術的な失敗が原因だった。
Friendster のアーキテクチャは、すべてを一つのデータベースに詰め込んでいた。ユーザープロフィール、友達関係、メッセージ、写真。3億人のユーザーデータが、一つの MySQL データベースに。
ページ読み込みに30秒。友達リストの表示に1分。ユーザーは離れていった。Facebook が台頭した。
Friendster のエンジニアは後に語った。「私たちは、スケールすることを考えていなかった。100万人で十分だと思っていた」。
教訓:
- 将来の成長を想定した設計
- データの分割戦略を最初から考える
- パフォーマンステストを継続的に実施
- アーキテクチャの見直しタイミングを逃さない
新しい潮流:サーバーレスとエッジ
2014年、AWS Lambda が発表された。
「サーバーを管理したくない」というエンジニアの願いが現実になった。コードを書いて、アップロードするだけ。スケーリング、可用性、セキュリティは AWS が面倒を見る。
// 従来のサーバー
const express = require('express');
const app = express();
app.post('/api/resize-image', upload.single('image'), async (req, res) => {
try {
const resized = await sharp(req.file.buffer)
.resize(200, 200)
.toBuffer();
res.type('image/jpeg').send(resized);
} catch (error) {
res.status(500).send('エラーが発生しました');
}
});
app.listen(3000);
// サーバーレス(AWS Lambda)
exports.handler = async (event) => {
const image = Buffer.from(event.body, 'base64');
const resized = await sharp(image)
.resize(200, 200)
.toBuffer();
return {
statusCode: 200,
headers: { 'Content-Type': 'image/jpeg' },
body: resized.toString('base64'),
isBase64Encoded: true
};
};
違いは明白だ。サーバーの起動、ポートのリスニング、プロセス管理。すべて不要。純粋にビジネスロジックだけを書けばいい。
Cloudflare Workers はさらに進んだ。エッジコンピューティング。ユーザーに最も近いデータセンターでコードを実行。東京のユーザーには東京で、ニューヨークのユーザーにはニューヨークで。
// Cloudflare Workers
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// 地理的に最適化されたレスポンス
const country = request.headers.get('CF-IPCountry');
if (country === 'JP') {
return new Response('こんにちは!日本からのアクセスですね。');
} else if (country === 'US') {
return new Response('Hello! Accessing from the United States.');
} else {
return new Response('Welcome from ' + country);
}
}
レイテンシは劇的に改善される。東京のユーザーが、カリフォルニアのサーバーを待つ必要はない。
アーキテクチャの未来
2030年、システムアーキテクチャはどうなっているだろうか。
量子コンピューティングが実用化され、従来の計算モデルが変わる。AIがアーキテクチャを自動設計する。ブロックチェーンが分散システムの信頼性を保証する。
しかし、基本原則は変わらない。関心の分離、疎結合、高凝集性、拡張性。これらの原則は、技術が変わっても生き続ける。
優れたアーキテクチャは、建築と同じだ。流行に左右されず、目的に忠実で、時の試練に耐える。パルテノン神殿が2400年後も立っているように、優れたソフトウェアアーキテクチャも、長く価値を提供し続ける。
あなたが次にシステムを設計する時、思い出してほしい。アーキテクチャは単なる技術的決定ではない。それは、未来への投資であり、チームへの贈り物であり、ユーザーへの約束だ。
正しいパターンを選び、適切に実装し、継続的に進化させる。それが、偉大なシステムを作る秘訣だ。