mybook

面接のフレームワーク — 4ステップで攻略する

「地図なしで迷子になっていた」

転職活動を始めて3週間が経ったソウタ(28歳、Webエンジニア歴4年)は、2社目の面接で完全に沈黙した。

「先週の面接、もう一度再現してみて」とレイカが言った。

「え……『Twitterを設計してください』と言われて、『データベースはPostgreSQLで……』と話し始めました。」

「そこで詰んでる。問題を渡された瞬間に設計を始めようとしたでしょ。それが間違い。」

「でも早く答えないとダメだと思って……」

「面接官はスピードを見ているんじゃない。思考のプロセスを見ている。」

レイカはホワイトボードに書いた。

❌ 問題を聞いた → 即座に設計を語る
✅ 問題を聞いた → まず地図を広げる

「システム設計面接には正しい順序がある。その順序を守るだけで、評価が大きく変わる。」


4ステップフレームワーク

Loading diagram...

「これが基本の地図。面接が60分なら、このくらいの時間配分で進める。守るだけで、少なくとも『どこから手をつければいいかわからない』という状態は回避できる。」

ソウタはメモを取った。合計40分。残りの20分はバッファと面接官の質問時間だ。


Step 1: 要件の明確化(5分)

「最初の5分は絶対に設計を始めない。要件を確認する時間だ。」

ソウタは驚いた。「5分も?」

「そう。むしろここを短縮したら負けだと思って。面接官はあなたが正しい問いを立てられるかを見ている。Amazonでシニアエンジニアを面接していたとき、いきなり設計を始める人の合格率は2割以下だった。」

機能要件 vs 非機能要件

要件には2種類ある。どちらを確認するかで、設計の方向性が完全に変わる。

機能要件(What — ユーザーが何をできるか)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- コアな機能は何か?
- ユーザーができることは?(投稿する、検索する、フォローする)
- スコープに含まれないものは?(明示的に除外する)

非機能要件(How — システムがどう動くか)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- ユーザー数・トラフィック規模は?(DAU/MAU)
- 可用性の要件は?(SLA: 99.9% vs 99.99%)
- レイテンシの要件は?(p99で何ミリ秒以内?)
- 一貫性の要件は?(強一貫性 vs 結果整合性)
- データ保持期間は?(ログを何年保つ?)
- 地理的要件は?(グローバル vs 単一リージョン)

INFO

機能要件と非機能要件は対称ではない。機能要件はスコープを絞るために使い、非機能要件は設計のボトルネックを予測するために使う。「可用性が99.9%でいい」と言われたら、Active-Activeのマルチリージョン設計は過剰だ。要件が制約を定義し、制約が設計を決める。

要件確認の技術 — ファンネル形式で絞り込む

闇雲に質問するのではなく、ファンネル形式で絞り込む。

1. まず機能の中心を確認する
   「このシステムで最も重要なコア機能は何ですか?」

2. スコープ外を明確にする
   「今日の設計に含めなくてよいものを確認させてください。
    リアルタイム通知は? 検索機能は? 分析ダッシュボードは?」

3. 非機能要件でスケールを把握する
   「想定するユーザー規模を教えてください。
    DAUはどのくらいですか? 地理的な分布は?」

4. 重要な制約を確認する
   「レイテンシの要件はありますか?
    データの一貫性はどの程度重要ですか?」

実際の要件確認ダイアログ

面接官から「Twitterを設計してください」と言われたとき:

ソウタ:「ありがとうございます。いくつか確認させてください。

まずコア機能についてです。
タイムラインの表示は必須ですか?(面接官:Yes)
フォロー/フォロワー機能は必要ですか?(Yes)
いいね、リツイートはスコープ内ですか?
  (今日はタイムラインに集中しましょう)
DMは?(対象外) 画像・動画の投稿は?(テキストのみでOK)

次に規模についてです。
ユーザー規模は?(1億DAU)
読み書きの比率はどのくらいを想定しますか?(100: 1)

最後に非機能要件を確認します。
タイムラインのレイテンシ要件は?(500ms以内)
可用性は?(99.9%以上)
データの整合性は?(結果整合性でOK)

了解しました。では要件をまとめます。
機能スコープ:ツイート投稿とホームタイムライン閲覧
規模:1億DAU、読み書き比率100:1
非機能:500ms以内、99.9%可用性、結果整合性
この理解で進めていいですか?」
# Railsモデルで要件を構造化して考える例
# 確認した要件をコードで表現すると思考が整理される
 
# 機能要件 → モデルの責務に対応する
class Tweet < ApplicationRecord
  belongs_to :user
  validates :content, presence: true, length: { maximum: 280 }
  # 画像は今回スコープ外 → has_many :media_attachments は不要
end
 
class Follow < ApplicationRecord
  belongs_to :follower, class_name: 'User'
  belongs_to :followed, class_name: 'User'
  validates :follower_id, uniqueness: { scope: :followed_id }
end
 
# 非機能要件 → インフラの制約に対応する
# DAU 1億、読み100:書き1 → キャッシュ重視の設計が必要
# レイテンシ500ms → データベース直接アクセスは避ける
# 結果整合性OK → タイムラインに多少の遅延は許容できる

WARNING

要件確認でよくある失敗:「何でも実装します」と言うこと。良い候補者はスコープを絞る。全部作るより、コア機能を完璧に設計する方が高評価につながる。「DMもリアルタイム通知も実装します」と言った瞬間、面接官は「この人は優先順位をつけられない」と判断する。


Step 2: 規模の概算(5分)

「次は数字。エンジニアは感覚で話してはいけない。スケールを数字で根拠を持って語るのがプロだ。」

「でも、具体的な数字なんてわからないですよね?」

「わからなくていい。オーダーオブマグニチュード(桁の精度)で考えれば十分。間違えるとしたら2倍か0.5倍程度の範囲で。10倍ずれなければ設計の方向性は変わらない。」

基本の数字を頭に入れる

# 面接前に暗記しておく数字(Rubyの定数として整理)
module ScaleConstants
  # 時間
  SECONDS_PER_DAY   = 86_400    # ≈ 10万秒と覚える
  SECONDS_PER_MONTH = 2_592_000  # ≈ 300万秒
  SECONDS_PER_YEAR  = 31_536_000 # ≈ 3000万秒
 
  # データサイズの目安
  TWEET_BYTES        = 300       # テキストツイート1件
  USER_RECORD_BYTES  = 1_000     # ユーザーレコード1件
  PHOTO_BYTES        = 1_000_000 # 写真1枚(圧縮後)
 
  # レイテンシ(Latency Numbers Every Programmer Should Know)
  L1_CACHE_NS      = 1    # L1キャッシュ参照: 1ns
  MEMORY_ACCESS_NS = 100  # メモリアクセス: 100ns
  SSD_READ_US      = 100  # SSD読み取り: 100μs
  HDD_READ_MS      = 10   # HDD読み取り: 10ms
  NETWORK_ROUND_MS = 150  # ネットワーク往復: 150ms(大陸間)
end

Twitterの場合の計算例

DAU: 1億 = 100M

ツイート投稿(書き込み)
━━━━━━━━━━━━━━━━━━━━━━━━
1日のツイート数: 100M DAU × 平均1ツイート = 100M tweets/day
QPS (書き込み): 100M / 86,400秒 ≒ 1,200 tweets/sec
ピーク時 (×5):  1,200 × 5 = 6,000 tweets/sec

タイムライン閲覧(読み込み)
━━━━━━━━━━━━━━━━━━━━━━━━
読み: 書き = 100: 1
読み込みQPS: 1,200 × 100 = 120,000 req/sec
ピーク時:    600,000 req/sec

ストレージ(テキストのみ)
━━━━━━━━━━━━━━━━━━━━━━━━
ツイート1件: 約300バイト
1日のストレージ: 100M × 300B = 30GB/day
5年間: 30GB × 365 × 5 ≒ 55TB

帯域
━━━━━━━━━━━━━━━━━━━━━━━━
読み込みQPS 120,000 × 300B = 36MB/sec
ピーク時: 180MB/sec ≈ 1.5Gbps
// Golangで概算計算を実装する例(面接中のメモとして使える)
package main
 
import "fmt"
 
type TwitterScale struct {
	DAU            int64
	ReadWriteRatio int64
	TweetSizeBytes int64
	PeakMultiplier int64
}
 
func (s *TwitterScale) WriteQPS() int64 {
	tweetsPerDay := s.DAU * 1 // 1ユーザー1日1ツイート
	return tweetsPerDay / 86400
}
 
func (s *TwitterScale) ReadQPS() int64 {
	return s.WriteQPS() * s.ReadWriteRatio
}
 
func (s *TwitterScale) StoragePerYear() int64 {
	tweetsPerDay := s.DAU * 1
	return tweetsPerDay * s.TweetSizeBytes * 365
}
 
func main() {
	scale := &TwitterScale{
		DAU:            100_000_000,
		ReadWriteRatio: 100,
		TweetSizeBytes: 300,
		PeakMultiplier: 5,
	}
	fmt.Printf("Write QPS: %d/sec\n", scale.WriteQPS())
	// ~1,157/sec
	fmt.Printf("Read QPS: %d/sec\n", scale.ReadQPS())
	// ~115,700/sec
	fmt.Printf("Storage/Year: %.0f GB\n",
		float64(scale.StoragePerYear())/1_000_000_000)
	// ~10.9 TB
}

INFO

計算は声に出しながらやること。面接官は答えではなく「どう考えているか」を見ている。「DAUが1億なので、1日の秒数が約10万、つまりQPSは……」と言いながら計算する。静かに計算して数字だけ出すのはNG。思考プロセスを見せるのが目的だ。

時間配分の失敗パターン

概算ステップでよく起こる失敗がある。

❌ 失敗パターン1:精度にこだわりすぎる
「正確なQPSを出すために、もう少し詳しい情報が……」
→ 桁の精度でいい。5分を超えたら次のステップへ。

❌ 失敗パターン2:計算をスキップする
「まあ大規模なので、Redisを使えばいいですね」
→ 数字なしに「大規模」と言っても意味がない。

❌ 失敗パターン3:全部を計算しようとする
「帯域も計算して、レイテンシも……」
→ QPS・ストレージ・帯域の3点を押さえれば十分。

✅ 正しいアプローチ
「概算なので多少誤差はありますが……」と断った上で
桁の精度で計算し、設計の方向性を確定させる。

Step 3: 高レベル設計(20分)

「ここからが本番。でもいきなり細部に入ってはいけない。まず全体の絵を描く。」

コンポーネントの「引き出し」を整理する

高レベル設計では、主要コンポーネントから選んで組み合わせる。それぞれの「使いどころ」を覚えておく。

Loading diagram...

各コンポーネントの使いどころ:

CDN(コンテンツ配信ネットワーク)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:静的アセット(画像、JS、CSS)、地理的に分散したユーザー
例:S3 + CloudFront、Cloudflare
効果:オリジンへのトラフィックを90%削減できる

ロードバランサー
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:APIサーバーを複数台に横展開するとき(常に必要)
種別:L4(TCP)vs L7(HTTP)。APIなら通常L7
例:AWS ALB、nginx

キャッシュ(Redis/Memcached)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:読み込みが多い、DBがボトルネック、計算コストが高い結果
種別:Read-Through、Write-Through、Write-Behind
注意:キャッシュ無効化戦略を必ず説明する

メッセージキュー(Kafka/SQS)
━━━━━━━━━━━━━━━━━━━━━━━━━━
使うとき:非同期処理、サービス間の疎結合、バースト対応
例:ツイート投稿→タイムライン配信(ファンアウト)
効果:ピーク時のスパイクを吸収できる

データベース(RDB vs NoSQL)
━━━━━━━━━━━━━━━━━━━━━━━━━━
RDB(PostgreSQL/MySQL):関係性が複雑、トランザクションが必要
NoSQL(DynamoDB/Cassandra):スケール重視、スキーマが柔軟
両方使う:ユーザー認証はRDB、タイムラインデータはNoSQL

INFO

コンポーネントを追加するたびに「なぜそれが必要か」を説明する。「Kafkaを使います」で終わるのではなく、「ツイート投稿とタイムライン配信を非同期にすることで、投稿者のレスポンスタイムを下げつつ、フォロワー数が多いユーザーのファンアウト処理をバックグラウンドで行えます」と語る。

Twitterの高レベル設計

Loading diagram...

「絵を描いたら、各コンポーネントの役割を1文で説明する。長々と語らなくていい。」

# Tweet API: ツイートの投稿と取得
class TweetsController < ApplicationController
  def create
    tweet = current_user.tweets.create!(tweet_params)
    # Kafkaにイベントを送信して非同期でファンアウト
    TweetPublishedEvent.publish(tweet_id: tweet.id, user_id: tweet.user_id)
    render json: tweet, status: :created
  end
 
  private
 
  def tweet_params
    params.require(:tweet).permit(:content)
  end
end
 
# Timeline API: Redisから事前構築済みタイムラインを返す
class TimelinesController < ApplicationController
  def show
    # カーソルベースのページネーションで一貫性を保つ
    tweet_ids = redis.lrange(
      "timeline:#{current_user.id}",
      0,
      (params[:limit] || 20).to_i - 1
    )
    tweets = Tweet.where(id: tweet_ids).index_by(&:id)
    render json: tweet_ids.map { |id| tweets[id.to_i] }.compact
  end
end
// Golang: Worker(Kafkaコンシューマー)でのファンアウト実装例
package worker
 
import (
	"context"
	"fmt"
 
	"github.com/segmentio/kafka-go"
)
 
type FanoutWorker struct {
	reader    *kafka.Reader
	redisPool RedisPool
	db        Database
}
 
func (w *FanoutWorker) Run(ctx context.Context) {
	for {
		msg, err := w.reader.ReadMessage(ctx)
		if err != nil {
			continue
		}
		tweetID := parseTweetID(msg.Value)
		w.fanout(ctx, tweetID)
	}
}
 
func (w *FanoutWorker) fanout(ctx context.Context, tweetID int64) {
	// フォロワー一覧を取得してそれぞれのタイムラインに追加
	followers := w.db.GetFollowers(ctx, tweetID)
	for _, followerID := range followers {
		key := fmt.Sprintf("timeline:%d", followerID)
		// LPUSHで先頭に追加、LTRIMで上限管理
		w.redisPool.LPush(ctx, key, tweetID)
		w.redisPool.LTrim(ctx, key, 0, 999) // タイムラインは1000件まで保持
	}
}

APIエンドポイントも定義する

設計の一部としてAPIを定義すると、コンポーネント間のインターフェースが明確になる。

POST /v1/tweets
  Request:  { "content": "Hello world" }
  Response: { "id": 123, "user_id": 456,
              "content": "...", "created_at": "..." }

GET /v1/timelines/home?cursor=<tweet_id>&limit=20
  Response: { "tweets": [...], "next_cursor": <tweet_id> }
# Rails routesとして表現する
Rails.application.routes.draw do
  namespace :v1 do
    resources :tweets, only: [:create, :show, :destroy]
    resource :timeline, only: [:show]
  end
end
 
# カーソルベースのページネーション実装
class V1::TimelineController < ApplicationController
  def show
    cursor_tweet_id = params[:cursor]&.to_i
    limit = (params[:limit] || 20).to_i.clamp(1, 100)
 
    tweet_ids = if cursor_tweet_id
      # カーソル以降のツイートIDをRedisから取得
      all_ids = redis.lrange("timeline:#{current_user.id}", 0, -1)
      start_idx = all_ids.index(cursor_tweet_id.to_s) || 0
      all_ids[start_idx + 1, limit]
    else
      redis.lrange("timeline:#{current_user.id}", 0, limit - 1)
    end
 
    tweets = Tweet.where(id: tweet_ids).order(created_at: :desc)
    render json: {
      tweets: tweets,
      next_cursor: tweets.last&.id
    }
  end
end

Step 4: 深掘りと改善(10分)

「最後は面接官と対話しながら、特定の部分を深掘りする。ここが一番差がつくフェーズだ。」

「どうやって面接官の関心を探るんですか?」

「直接聞けばいい。」

面接官との対話の技術

深掘りフェーズは相互の会話だ。面接官の関心を引き出してから、そこに集中する。

ステップ1:主導権を渡す
「今のデザインで気になる点があれば教えてください。
 どのコンポーネントを深掘りしたいですか?」

ステップ2:懸念を受け止める
「なるほど、タイムラインのスケーラビリティが懸念ですね。
 それについて詳しく話しましょう。」

ステップ3:トレードオフを語る
「現在のFan-out-on-writeアプローチには〇〇という問題があります。
 代替としてFan-out-on-readが考えられますが、トレードオフとして
 読み込み時の遅延が増加します。今回の要件(500ms以内)を考えると……」

ステップ4:自分の判断を示す
「私は〇〇を選択します。理由は……」

面接官のフィードバック会話

実際の面接官はこんなふうにプッシュしてくる。


面接官: 「セレブリティアカウント(フォロワー数千万人)がツイートしたとき、Fan-out-on-writeではどうなりますか?」

ソウタ: 「そこは懸念点です。フォロワーが1000万人いると、ファンアウトに1000万件の書き込みが必要になります。計算すると、1000万件 × 300バイト = 3GBのRedis書き込みが発生します。Workerのスループットに問題が出ます。」

面接官: 「どう解決しますか?」

ソウタ: 「2つのアプローチがあります。ひとつはハイブリッド方式です。フォロワー数が閾値(例:100万人)を超えるアカウントはFan-out-on-readに切り替え、タイムライン取得時にマージします。もうひとつは優先キューです。セレブリティのファンアウトを低優先度キューに分離して、通常ユーザーへの影響を排除します。私は前者を選択します。実装が複雑になりますが、読み込みQPS(120,000/sec)を考えると、セレブリティのリアルタイム性を多少犠牲にしてでもスループットを確保する方が合理的です。」


# ハイブリッド方式のタイムライン取得
class HybridTimelineService
  CELEBRITY_THRESHOLD = 1_000_000
 
  def get_home_timeline(user_id, limit: 20)
    # 1. Redisから通常ユーザーのプリビルドタイムラインを取得
    cached_ids = redis.lrange("timeline:#{user_id}", 0, limit * 2 - 1)
 
    # 2. フォロー中のセレブリティのツイートをDBから直接取得
    celebrity_followings = Follow
      .where(follower_id: user_id)
      .joins(:followed)
      .where("users.followers_count > ?", CELEBRITY_THRESHOLD)
      .pluck(:followed_id)
 
    celebrity_tweets = Tweet
      .where(user_id: celebrity_followings)
      .order(created_at: :desc)
      .limit(limit)
 
    # 3. マージしてソート
    cached_tweets = Tweet.where(id: cached_ids)
    (cached_tweets + celebrity_tweets)
      .uniq(&:id)
      .sort_by { |t| -t.created_at.to_i }
      .first(limit)
  end
end

よくある深掘りポイントと回答の型

単一障害点(SPOF)
「現在の設計でSPOFはAPIサーバーです。
 ロードバランサーの背後に複数台配置し、ヘルスチェックで
 障害ノードを自動除外します。Redisはレプリカセットを組みます。」

データベースのスケーリング
「最初はリードレプリカで読み込みをスケール。
 書き込みがボトルネックになったらシャーディングを検討します。
 Twitterの場合、user_idでシャーディングするとツイートの
 局所性が保たれます。」

キャッシュの無効化
「今回のタイムラインキャッシュはWrite-Throughです。
 ツイートが削除された場合、Workerがタイムラインから該当IDを
 削除します。厳密なリアルタイム性より結果整合性を選択しています。」

WARNING

深掘りフェーズの注意点:面接官に誘導されて設計を全部作り直そうとしてはいけない。「その懸念はもっともです。こういった対策が考えられますが、トレードオフとして〇〇があります」と落ち着いてトレードオフを語る。完璧な設計は存在しない——それを面接官は知っている。あなたが「問題を認識して、理由を持って選択できるか」を見ている。

トレードオフの語り方テンプレート

面接で使えるトレードオフの語り方。これを覚えておくと迷わない。

「[選択肢A] と [選択肢B] があります。
 [選択肢A] は [メリット] がありますが、[デメリット] という
 トレードオフがあります。
 今回の要件 [要件の引用] を考慮すると、
 私は [選択] を選択します。
 理由は [理由] です。」

具体例:

「Fan-out-on-write(プッシュ型)とFan-out-on-read(プル型)があります。
 プッシュ型は読み込み時が高速ですが、フォロワー数が多いと
 書き込みコストが高くなるトレードオフがあります。
 今回の要件(タイムライン表示500ms以内、読み書き比率100:1)を
 考慮すると、私はプッシュ型をベースに、セレブリティアカウントは
 例外的にプル型を混在させるハイブリッド方式を選択します。
 理由は読み込みが圧倒的に多い場合、プッシュ型の方がユーザー体験
 に直結するからです。」

4ステップを練習する方法

「フレームワークを知っただけでは使えない。体に染み込ませる必要がある。」

「どう練習すればいいですか?」

「Mock面接を週2回やれ。自分でホワイトボードに向かうか、友人と組む。」

Mock面接の進め方

準備(5分)
━━━━━━━━━━━━━━━━━━━━━━━━
- タイマーを用意する(Step別に管理)
- ホワイトボード or 紙を用意する
- 設計する問題を1つ選ぶ

実践(40分)
━━━━━━━━━━━━━━━━━━━━━━━━
Step 1(5分タイマー):声に出して要件確認
Step 2(5分タイマー):計算を声に出しながら概算
Step 3(20分タイマー):コンポーネント図を描きながら説明
Step 4(10分タイマー):ボトルネックを1つ選んで深掘り

振り返り(15分)
━━━━━━━━━━━━━━━━━━━━━━━━
- どのステップで時間をオーバーしたか?
- トレードオフを語れたか?
- 数字は出せたか?
# 練習ログを記録するモデル(実際に記録をつけると上達が早い)
class MockInterviewSession
  SCORING_CRITERIA = {
    requirements_clarity: "要件確認でスコープを絞れたか",
    scale_estimation:     "数字で根拠を示せたか",
    component_design:     "コンポーネントの役割を説明できたか",
    tradeoff_discussion:  "トレードオフを語れたか",
    time_management:      "各ステップの時間を守れたか"
  }.freeze
 
  attr_reader :problem, :scores
 
  def initialize(problem)
    @problem = problem
    @scores  = SCORING_CRITERIA.keys.each_with_object({}) { |k, h| h[k] = 0 }
  end
 
  def evaluate(criterion, score_0_to_5)
    @scores[criterion] = score_0_to_5
  end
 
  def weakest_area
    scores.min_by { |_, v| v }.first
  end
 
  def report
    SCORING_CRITERIA.each do |key, label|
      puts "#{label}: #{scores[key]}/5"
    end
    puts "最も改善が必要な領域: #{SCORING_CRITERIA[weakest_area]}"
  end
end

推奨する練習問題(難易度順)

入門
━━━━━━━━━━━━━━━━━━━━━━━━
- URL短縮サービス(bit.ly)
- ペーストビン(pastebin.com)

中級
━━━━━━━━━━━━━━━━━━━━━━━━
- チャットシステム(LINE/Slack)
- 検索オートコンプリート
- レート制限システム

上級
━━━━━━━━━━━━━━━━━━━━━━━━
- Twitterのタイムライン
- YouTubeの動画配信
- Uber のリアルタイムマッチング

フレームワークをまとめると

# 面接の進め方(擬似コード)
def system_design_interview(problem)
  # Step 1: 要件確認(5分)
  # ファンネル形式で機能→スコープ外→非機能の順に確認
  requirements = clarify_requirements(problem)
  confirm_understanding(requirements) # 「この理解で進めていいですか?」
 
  # Step 2: 規模概算(5分)
  # 声に出しながら計算。桁の精度でいい
  scale = estimate_scale(requirements)
  # QPS、ストレージ、帯域の3点を押さえる
 
  # Step 3: 高レベル設計(20分)
  # 4ボックス(クライアント→LB→サービス→DB)から始める
  design = high_level_design(requirements, scale)
  # コンポーネントを追加するたびに「なぜ」を説明する
 
  # Step 4: 深掘り(10分)
  # 面接官の関心を聞いてから、トレードオフを語る
  deep_dive = refine_with_tradeoffs(design, interviewer_concerns)
  # 「〜という懸念はもっともです。A vs Bのトレードオフとして……」
end

「地図が手に入った気がします」

セッションを終えて、ソウタは言った。

「なるほど。前回の面接は要件確認を完全に飛ばして、いきなり実装を考えていました。それで詰まったんですね。」

「そう。そして数字も出さなかった。面接官には『この人はスケールのことを考えていない』と見えていたはず。要件確認をしないエンジニアは、ゴールがわからないまま走るのと同じだ。」

「4ステップを守れば、次は大丈夫でしょうか。」

レイカは少し笑った。「守るだけじゃ足りない。フレームワークは地図であって、目的地じゃない。大事なのはトレードオフを語る力——なぜその設計を選んだか、何を犠牲にしたかを説明できることだ。」

「次の課題は何ですか?」

数字で考える力を鍛えること。計算が苦手でも、桁の感覚さえあれば面接は乗り越えられる。毎日1つ、身近なサービスのQPSを概算してみな。LINEのメッセージ送信数は? Netflixのストリーミング帯域は? それを繰り返すだけで、半月後には感覚が変わる。」

ソウタはノートに書いた。「今日のLINEのQPS概算」——これが次のホームワークだ。


次章では、バックオブエンベロープ計算——あらゆる設計の基礎となる「概算の技術」を体系的に学ぶ。