メインコンテンツへスキップ
上級30分で読める

システムアーキテクチャの基本パターン

システム設計における基本的なアーキテクチャパターンを分かりやすく解説。レイヤードアーキテクチャ、マイクロサービス、MVC、イベント駆動など、実践的なパターンとその適用場面を学びます。

システム開発アーキテクチャシステム設計設計パターンマイクロサービスMVC

🎯 この記事で学べること

  • 1
    システムアーキテクチャの重要性と設計原則を理解できます
  • 2
    レイヤードアーキテクチャとその実装方法を習得できます
  • 3
    MVCや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万人で十分だと思っていた」。

教訓:

  1. 将来の成長を想定した設計
  2. データの分割戦略を最初から考える
  3. パフォーマンステストを継続的に実施
  4. アーキテクチャの見直しタイミングを逃さない

新しい潮流:サーバーレスとエッジ

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年後も立っているように、優れたソフトウェアアーキテクチャも、長く価値を提供し続ける。

あなたが次にシステムを設計する時、思い出してほしい。アーキテクチャは単なる技術的決定ではない。それは、未来への投資であり、チームへの贈り物であり、ユーザーへの約束だ。

正しいパターンを選び、適切に実装し、継続的に進化させる。それが、偉大なシステムを作る秘訣だ。