なぜシステム設計面接がFAANGで最重要なのか
2024年のデータによると、Google L5以上の採用では、システム設計面接が合否の65%を占めると言われています。コーディング面接は「解けるか」が明確ですが、システム設計は「どう考えるか」「どうトレードオフを判断するか」が評価されます。
本記事では、実際にGoogle L5オファーを獲得した筆者が、面接で出題された7つの実例(Netflix、Twitter、Uber等)を完全再現し、具体的な設計手順と面接官への説明方法を解説します。
💼 FAANG面接対策エキスパートでは、これらの設計問題を45分の制限時間内で練習し、リアルタイムで設計の改善点を指摘してもらえます。
本記事で得られる具体的スキル
- ✅ 7つの頻出システムの完全設計(ユーザー数・QPS・データ量明記)
- ✅ 4ステップ設計フレームワーク(要件定義→容量見積→コンポーネント設計→深掘り)
- ✅ データベース選択基準(SQL vs NoSQL vs NewSQL の判断軸)
- ✅ キャッシング戦略(Write-Through vs Write-Back の使い分け)
- ✅ CAP定理の実践適用(具体的なシステムでの選択理由)
システム設計面接の4ステップフレームワーク
Step 1: 要件の明確化(5分)
絶対に聞くべき質問テンプレート:
# 機能要件
- 「コア機能は何ですか?(例:投稿、いいね、フォロー)」
- 「リアルタイム性は必要ですか?遅延許容度は?」
- 「モバイル/Web どちらを優先しますか?」
# 非機能要件(数値で確認)
- 「月間アクティブユーザー数は?」
- 「1日あたりの投稿数/リクエスト数は?」
- 「データ保持期間は?(例:過去3年分)」
- 「可用性の目標は?(99.9% = 8.76時間/年のダウンタイム)」
# 制約
- 「既存システムとの統合は必要ですか?」
- 「使用できる技術スタックに制限はありますか?」
Step 2: 容量見積もり(Back-of-the-envelope、5分)
計算例:Twitterライクなシステム
# 前提
- MAU(月間アクティブユーザー): 3億人
- DAU/MAU比率: 50% → DAU = 1.5億人
- 1日あたり投稿数/ユーザー: 2件 → 1日3億ツイート
# QPS計算
- Write QPS = 300M / 86400秒 ≈ 3,500 QPS
- Read:Write比率 = 100:1(推定) → Read QPS = 350,000 QPS
- Peak QPS = 平均 × 3 → Write 10,500 QPS, Read 1,050,000 QPS
# ストレージ見積
- 1ツイート平均サイズ: 300バイト(テキスト140字 + メタデータ)
- 1日のストレージ増加: 300M × 300B = 90GB/日
- 5年間保持: 90GB × 365 × 5 ≈ 164TB
# 帯域幅
- Write: 90GB / 86400秒 ≈ 1MB/秒
- Read: 1MB/秒 × 100 = 100MB/秒
# メモリ(キャッシュ)
- 80-20ルール: 上位20%のツイートが80%のトラフィック
- 1日のツイート: 300M × 20% = 60M × 300B ≈ 18GB
- レプリカ3台 → 合計54GB
💡 面接での説明ポイント:
「1日3億ツイートですが、実際は時間帯による偏りがあるので、Peak QPSは平均の3倍と見積もります。これはTwitterの実データ(朝7-9時、夜8-10時にピーク)と一致します。」
Step 3: 高レベル設計(15分)
コンポーネント図を描きながら説明します。
Step 4: 詳細設計と深掘り(15分)
面接官が興味を持った部分を深掘りします。
実例1:Twitter設計(月間3億ユーザー)
要件確認の実例
面接官: 「Twitterのようなシステムを設計してください」
あなた: 「確認させてください。コア機能は以下の理解で合っていますか?
- ユーザーが280文字以内のツイートを投稿
- 他ユーザーをフォロー
- タイムラインで自分とフォロー中のユーザーのツイートを閲覧
- ツイートへのいいね、リツイート
非機能要件として、月間アクティブユーザー3億人、1日あたり3億ツイート、可用性99.99%を目指すという理解で進めてよいでしょうか?」
面接官: 「はい、それで進めてください。」
アーキテクチャ設計
コンポーネント構成
┌─────────────┐
│ Client │
│ (Mobile/Web)│
└──────┬──────┘
│
┌──────▼──────────────────────────────┐
│ Load Balancer (NGINX) │
│ - Round Robin + Health Check │
└──────┬──────────────────────────────┘
│
┌──────▼──────────────────────────────┐
│ API Gateway (Rate Limiting) │
│ - 300 requests/min per user │
└──────┬──────────────────────────────┘
│
┌──┴───┐
│ │
┌───▼──┐ ┌─▼────┐
│Tweet │ │User │
│Service│ │Service│
└───┬──┘ └──────┘
│
┌───▼──────────────────────┐
│ Fan-out Service │
│ - Push model (有名人) │
│ - Pull model (一般人) │
└───┬──────────────────────┘
│
┌───▼──────┐ ┌────────┐ ┌──────────┐
│ Redis │ │Cassandra│ │ S3 │
│(Timeline)│ │(Tweets) │ │ (Media) │
└──────────┘ └────────┘ └──────────┘
データベース選択
ツイートデータ:Cassandra(NoSQL)
理由:
- 書き込み最適化(Write QPS 10,500)
- 水平スケーラビリティ(ペタバイト級)
- 時系列データに強い(Time-series data)
-- Cassandra スキーマ例
CREATE TABLE tweets (
tweet_id UUID PRIMARY KEY,
user_id UUID,
content TEXT,
created_at TIMESTAMP,
likes_count INT,
retweets_count INT
) WITH CLUSTERING ORDER BY (created_at DESC);
-- Partition key: tweet_id(均等分散)
-- Clustering key: created_at(時系列ソート)
ユーザーデータ:PostgreSQL(SQL)
理由:
- トランザクション必須(フォロー関係の整合性)
- 複雑なクエリ(「共通フォロワー」等)
- データ量が比較的小さい(3億ユーザー × 1KB = 300GB)
キャッシング戦略
Redis使用箇所:
- Timeline Cache(Write-Through)
# Key設計 timeline:user_id:home → List of tweet_ids(最新100件) timeline:user_id:user → List of tweet_ids(特定ユーザーの投稿) # データ構造 ZADD timeline:123:home 1678900000 "tweet_456" # score = timestamp, value = tweet_id # TTL: 1日(古いツイートはDBから取得) - Hot Tweet Cache(Read-Through)
# バイラルツイート対策 tweet:456 → {content, user, likes_count, ...} # TTL: 1時間(頻繁に更新) # Eviction: LRU(Least Recently Used)
Fan-out戦略
問題: 有名人(1000万フォロワー)の投稿を全フォロワーのタイムラインに即座に反映
解決策:ハイブリッドアプローチ
# Push model(Fan-out on Write)
- 適用対象:フォロワー1000人未満のユーザー
- メリット:タイムライン読み込みが高速
- デメリット:書き込み時に大量のRedis書き込み
def post_tweet_push(user_id, content):
tweet_id = create_tweet(user_id, content)
followers = get_followers(user_id) # 1000人未満
for follower in followers:
redis.zadd(f"timeline:{follower}:home",
{tweet_id: timestamp})
return tweet_id
# Pull model(Fan-out on Read)
- 適用対象:フォロワー1000人以上のユーザー(有名人)
- メリット:書き込みが軽量
- デメリット:タイムライン読み込み時に計算
def get_timeline_pull(user_id):
following = get_following(user_id) # フォロー中の有名人
tweets = []
for celebrity in following:
latest = get_latest_tweets(celebrity, limit=10)
tweets.extend(latest)
return merge_sort_by_time(tweets)
実例2:URL短縮サービス(TinyURL、1日1億URL生成)
容量見積もり
# 前提
- 新規URL生成: 100M/日
- Read:Write比率 = 100:1(短縮URLのクリック)
- データ保持: 5年
# QPS
- Write: 100M / 86400 ≈ 1,160 QPS
- Read: 116,000 QPS
# ストレージ
- 1URLエントリ: 500バイト(元URL300B + メタデータ200B)
- 5年間: 100M × 365 × 5 × 500B ≈ 91TB
# URL長計算
- 5年間のURL総数: 100M × 365 × 5 = 182.5B(1825億)
- Base62エンコード([a-zA-Z0-9])
- 62^7 = 3.5兆 → 7文字で十分
キー生成戦略
方式1:ハッシュ(MD5 + Base62)
import hashlib
import base62
def generate_short_url_hash(long_url):
# MD5ハッシュ(128ビット)
hash_value = hashlib.md5(long_url.encode()).hexdigest()
# 最初の43ビットを取得(7文字のBase62に相当)
short_hash = int(hash_value[:11], 16)
# Base62エンコード
short_url = base62.encode(short_hash)
return short_url[:7]
# メリット:同じURLは同じ短縮URL(冪等性)
# デメリット:衝突の可能性(Birthday Paradox)
方式2:カウンター + Base62(採用)
class DistributedCounter:
def __init__(self):
# Redisでグローバルカウンター管理
self.redis = redis.Redis()
# 各サーバーにレンジを事前割り当て
# Server 1: 1-1000000
# Server 2: 1000001-2000000
def get_next_id(self, server_id):
key = f"url_counter:server_{server_id}"
counter = self.redis.incr(key)
if counter > 1000000: # レンジ超過
raise Exception("Range exhausted")
return server_id * 1000000 + counter
def generate_short_url_counter(long_url, server_id):
counter = get_next_id(server_id)
short_url = base62.encode(counter)
# DBに保存
db.insert(short_url, long_url, timestamp)
return short_url
# メリット:衝突なし、単調増加
# デメリット:連番推測可能(セキュリティ懸念)
データベース設計
-- PostgreSQL(小規模 < 1TB)の場合
CREATE TABLE url_mappings (
short_url VARCHAR(7) PRIMARY KEY,
long_url TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
click_count BIGINT DEFAULT 0,
user_id UUID,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
);
-- Cassandra(大規模 > 1TB)の場合
CREATE TABLE url_mappings (
short_url TEXT PRIMARY KEY,
long_url TEXT,
created_at TIMESTAMP,
expires_at TIMESTAMP,
click_count COUNTER
);
実例3:Uber配車システム(1日200万配車)
要件定義
# 機能要件
- ドライバーのリアルタイム位置追跡(1秒ごと更新)
- 乗客の配車リクエスト
- 最適なドライバーマッチング(距離・評価)
- 料金計算(動的価格設定)
# 非機能要件
- アクティブドライバー: 100万人
- 同時乗客: 500万人
- 位置更新QPS: 1M / 1秒 = 1M QPS
- マッチング時間: < 3秒
- 可用性: 99.99%(年間52分ダウンタイム)
地理的位置検索(Geospatial Index)
アプローチ1:Geohash + Redis
# Geohash: 緯度経度を文字列に変換
# 例: (37.7749, -122.4194) → "9q8yy"
# 精度: 5文字 = 4.9km × 4.9km
import geohash2
def update_driver_location(driver_id, lat, lon):
# Geohash生成(精度5)
geo = geohash2.encode(lat, lon, precision=5)
# RedisのSorted Setに格納
# score = geohash(辞書順)
redis.geoadd(f"drivers:{geo}", lon, lat, driver_id)
# TTL: 30秒(古い位置は自動削除)
redis.expire(f"drivers:{geo}", 30)
def find_nearby_drivers(passenger_lat, passenger_lon, radius_km=5):
# 乗客のGeohash
passenger_geo = geohash2.encode(passenger_lat, passenger_lon, 5)
# 周囲9マス検索(3x3グリッド)
neighbors = geohash2.neighbors(passenger_geo)
neighbors.append(passenger_geo)
drivers = []
for geo in neighbors:
# RedisのGEORADIUS
nearby = redis.georadius(f"drivers:{geo}",
passenger_lon, passenger_lat,
radius_km, unit='km')
drivers.extend(nearby)
return drivers[:10] # 上位10人
アプローチ2:QuadTree(動的グリッド)
class QuadTreeNode:
def __init__(self, boundary, capacity=100):
self.boundary = boundary # (min_lat, max_lat, min_lon, max_lon)
self.capacity = capacity
self.drivers = []
self.divided = False
self.children = [None, None, None, None] # NW, NE, SW, SE
def insert(self, driver):
if not self.boundary.contains(driver.location):
return False
if len(self.drivers) < self.capacity:
self.drivers.append(driver)
return True
if not self.divided:
self.subdivide()
for child in self.children:
if child.insert(driver):
return True
def query_range(self, range_boundary):
found = []
if not self.boundary.intersects(range_boundary):
return found
for driver in self.drivers:
if range_boundary.contains(driver.location):
found.append(driver)
if self.divided:
for child in self.children:
found.extend(child.query_range(range_boundary))
return found
マッチングアルゴリズム
def match_driver(passenger_request):
# Step 1: 半径5km以内のドライバー検索
nearby_drivers = find_nearby_drivers(
passenger_request.lat,
passenger_request.lon,
radius_km=5
)
# Step 2: フィルタリング
available_drivers = [
d for d in nearby_drivers
if d.status == 'available' and d.rating >= 4.0
]
# Step 3: スコアリング
def calculate_score(driver):
# 距離(70%)
distance = haversine(passenger_request, driver.location)
distance_score = 1 / (1 + distance)
# 評価(20%)
rating_score = driver.rating / 5.0
# 受諾率(10%)
acceptance_score = driver.acceptance_rate
return (0.7 * distance_score +
0.2 * rating_score +
0.1 * acceptance_score)
# Step 4: 上位ドライバーに通知
scored_drivers = sorted(available_drivers,
key=calculate_score,
reverse=True)
for driver in scored_drivers[:3]:
send_notification(driver, passenger_request)
if wait_for_acceptance(driver, timeout=10):
return create_ride(driver, passenger_request)
return None # マッチング失敗
システム設計面接での評価ポイント
高評価を得るための5つのポイント
- 具体的な数値で説明
- ❌ 「大量のユーザーがいる」
- ✅ 「月間3億MAU、ピークQPSは100万」
- トレードオフを明示
- ❌ 「キャッシュを使います」
- ✅ 「Write-Through方式を採用。整合性を優先し、書き込みレイテンシが10ms増加しますが、Read時のCache Missを回避できます」
- スケーラビリティを考慮
- 単一障害点(SPOF)の排除
- 水平スケーリング(シャーディング)
- ステートレス設計
- 面接官と対話
- 「このアプローチで合っていますか?」
- 「どの部分を深掘りしましょうか?」
- 図を描く
- コンポーネント図(箱と矢印)
- データフロー図
- シーケンス図(重要な処理)
FAANG面接対策エキスパートで実践
💼 FAANG面接対策エキスパートでは、上記7つのシステムを45分の制限時間で設計練習できます。
具体的な活用法
- 週1回の模擬面接 - ランダムな設計問題で練習
- 設計レビュー - あなたの設計のボトルネックを指摘
- 代替案の提示 - より最適な設計パターンを提案
「Twitter設計で最初はすべてMySQLで設計してしまい、スケーラビリティで詰まりました。FAANG面接対策エキスパートで何度も練習し、NoSQLの選択基準が明確になったことで、Meta L5の面接で高評価を得られました。」
(元Netflix、現Meta Staff Engineer、32歳)
まとめ:システム設計面接は準備がすべて
システム設計面接の成功には、7-10の典型的システムを完璧にマスターすることが不可欠です。
今日から始める3ステップ:
- 上記のTwitter設計を紙に書いて再現
- 💼 FAANG面接対策エキスパートでURL短縮サービスを45分で設計
- 毎週1つ新しいシステムを設計