プロローグ — 面接で「設計してください」と言われた日
面接前夜
2024年10月14日、日曜日の深夜1時。
田中ソウタはノートパソコンの画面を前に、LeetCodeの「Medium」タグの問題をひたすら解き続けていた。28歳、Webエンジニア歴4年。現職はRails製の中規模ECサイトを運営するスタートアップで、AWSのEC2やRDSを使った実務経験もある。
翌日はFinTech系スタートアップ「PayScale」の最終面接だった。
PayScaleはシリーズBで50億円を調達し、国内の決済インフラを再構築しようとしている注目企業だ。求人票には「年収800万〜1200万円」と書いてあった。ソウタの現職の年収は480万円。人生を変えるかもしれない面接だと思った。
「コーディングテストは余裕だった。アルゴリズムの問題も準備している。」
ソウタは自分に言い聞かせながら、最後に「Railsのパフォーマンスチューニング」についての回答をノートにまとめた。N+1問題、eager loading、インデックスの貼り方。自分の得意分野だ。
でも、「システム設計の問題が出るかもしれない」という不安を、ソウタは夜中まで無視し続けた。
「まあ、実務経験があるから大丈夫だろう。」
不意打ちの一言
翌朝、ソウタは渋谷のオフィスビル20階の会議室に通された。
面接官は2人。ひとりは40代くらいのエンジニアリングマネージャーで、もうひとりは30代前半に見えるシニアエンジニアだった。名刺には「CTO補佐」と書いてあった。
最初の45分は順調だった。
RailsのActiveRecordについての質問。AWSのECSを使ったデプロイ経験。過去のプロジェクトでのトラブル対応。ソウタは準備した回答を丁寧に説明した。面接官たちもうなずきながら聞いてくれた。
「コーディング問題も見てみましょうか」とCTO補佐が言い、ホワイトボードに問題を書いた。二分探索木の実装。ソウタは15分で完璧に解いた。
「いい感じだ」とソウタは思った。「このまま終わればいい。」
「では最後に」とエンジニアリングマネージャーが言った。「Twitterのようなタイムラインサービスを設計してください。」
ソウタの頭が真っ白になった。
30分間の迷走
「え、えーと……データベースはPostgreSQLを使って……」
ソウタは慌てて話し始めた。テーブル設計を頭に思い浮かべ、ER図を脳内で描こうとした。しかし、面接官の次の一言がソウタを完全に止めた。
「ユーザーが1億人いる場合はどうしますか?」
(ソウタの思考・混乱のログ)
1億人……PostgreSQL 1台では無理……シャーディング?
でも、シャーディングってどうやって実装するんだっけ……
レプリカ? read replica? でも具体的な数字が出てこない……
タイムラインってどうやって生成するんだっけ?
フォローしている全員の投稿を取ってきて……
全部のツイートをSELECTして……いや、それは遅い……
Push方式? Pull方式? なんかどっかで読んだ気がする……
Redisを使えばいいのか? キャッシュするの? 何を?
CDNって関係する? ……わからない
とにかく何か言わないと……
「えーと、まずユーザーテーブルとツイートテーブルとフォローテーブルを作って……」
「そのテーブル設計で、特定ユーザーのタイムライン取得のクエリはどう書きますか?」
ソウタはホワイトボードにSQLを書いた。
-- ソウタが書いたSQL(面接中)
SELECT tweets.*
FROM tweets
JOIN follows ON follows.followed_id = tweets.user_id
WHERE follows.follower_id = :current_user_id
ORDER BY tweets.created_at DESC
LIMIT 20;「このクエリ、1億ユーザーで1人あたり平均500人をフォローしていたら、どれくらいの時間がかかりますか?」
「…………」
ソウタは答えられなかった。数字が頭に浮かばなかった。
「Railsで書いてみてもいいですよ」とCTO補佐がフォローしてくれた。
# ソウタがホワイトボードに書いたコード(面接中)
def timeline(user)
user.following.flat_map(&:tweets).sort_by(&:created_at).reverse.first(20)
end「これはN+1問題がありますね。どう直しますか?」
「eager loadingで……」とソウタは言いかけて止まった。根本的な問題はN+1じゃないと気づいたからだ。でも、じゃあ何が問題なのか、どう説明すればいいのか、言語化できなかった。
30分後、面接は終わった。ソウタは「後日ご連絡します」という言葉とともに部屋から出された。
渋谷の街を歩きながら、ソウタは自分が何を言ったか、何を言えなかったか、ぼんやりと思い返した。電車の中でも、夕飯を食べながらも、ずっとあの30分間が頭から離れなかった。
結果は不合格。3日後のフィードバックのメールにはこう書いてあった。
スケーラビリティへの対応と、設計の根拠を言語化する力について、もう少し深掘りして頂けると良かったと思います。技術的な基礎知識はしっかりお持ちですが、大規模システムの設計議論においては、トレードオフを定量的に語れることが重要です。
自己分析——なぜ詰まったのか
不合格のメールを受け取った夜、ソウタはノートに書き出した。
「なぜ自分は詰まったのか。」
1. 要件を確認しなかった
タイムラインサービスと言われて、すぐに実装を考え始めた。でも「何を作るか」を明確にしないまま設計を始めるのは、目的地を決めずにカーナビを起動するようなものだ。
DAU(デイリーアクティブユーザー)は? 書き込みと読み込みの比率は? リアルタイム性はどれくらい必要か? フォロワー数の上限は? これらを一切聞かなかった。
2. 数字で考えられなかった
「1億ユーザー」と言われて思考が止まった。でも冷静に考えれば、1億人が1日10ツイートするとして、1秒あたりのリクエスト数は計算できる。数字があれば、どのコンポーネントをどのスケールで設計するか、具体的に話せる。
3. フレームワークがなかった
「システムを設計してください」と言われたとき、何から始めるか分からなかった。要件確認 → 見積もり → 高レベル設計 → 詳細設計、という順番があることを知らなかった。
4. トレードオフを語れなかった
Push方式とPull方式、どちらにするか。両方の長所と短所を比較して選択する、という思考の型がなかった。
WARNING
実務でRailsやAWSを使っている経験は、システム設計面接では直接役立たない。面接で求められるのは「スケールを想定した設計思考」と「トレードオフの言語化」だ。普段の業務でそれを意識していない人は、準備なしで面接に臨むと必ず詰まる。
Meetupでの出会い
不合格から一週間後の土曜日、ソウタはエンジニア向けのMeetup「Tech Architecture Night」に参加した。
落ち込んでいたが、家にいても仕方ない。会場は代官山のコワーキングスペースで、50人くらいのエンジニアが集まっていた。
登壇者のセッションを聞きながらビールを飲んでいると、隣に女性が座った。30代後半くらい。Tシャツにジーンズ、ノーメイクに近いシンプルな格好だったが、どこか存在感があった。
「面白くないですよね、このセッション。」
突然そう言われてソウタは驚いた。
「え……まあ、少し基礎的すぎるかなとは……」
「Googleのマイクロサービスの話を、実際に運用したことない人間が説明してる。理論だけで実運用の勘所がない。」
辛辣だった。でも的確だった。
懇親会で隣になり、自己紹介をした。中村レイカ。元Amazon Web ServicesのシニアSolutions Architect。現在は独立してエンジニアのキャリアコーチをしているという。
ソウタは面接での失敗談を話した。酔いも手伝って、正直に全部話した。詰まったこと、言語化できなかったこと、フィードバックのメールを何度も読み返したこと。
レイカは静かに聞いた。
「ふーん」と言った。「よくある負け方ね。」
「よくある、ですか。」ソウタは少し腹が立った。
「そう。毎年何十人もそのパターンで落ちる人を見てる。コーディングは完璧。実務経験もある。でも設計面接だけはボロボロ。なぜだと思う?」
「……フレームワークがないから、ですかね。今週ずっと考えてたんですけど。」
レイカは少し目を細めた。
「自己分析はできてる。それは良い。システム設計面接で落ちるパターンは全部同じ。要件を確認しない。数字で考えない。フレームワークを持っていない。」
「じゃあ、どうすれば良かったんですか」
「1ヶ月、私のやり方で練習すれば受かる。保証はしないけど、少なくとも面接官に『この人は考え方がしっかりしている』と思わせるくらいにはなれる。」
レイカは名刺を差し出した。
中村レイカ
元 Amazon Web Services Senior Solutions Architect
現 エンジニアキャリアコーチ
ソウタはその名刺をじっと見た。AWSのロゴが印刷されていない、シンプルな白い名刺だった。
「……お願いします。」
システム設計面接とは何か
翌週の月曜日、レイカのオフィスでの最初のセッションが始まった。
代々木上原の小さなマンションの一室。壁一面がホワイトボードになっていた。
「まず根本的な話から」とレイカは言った。「そもそもなぜ、会社はシステム設計面接をするのかわかる?」
「設計の能力を見るため……じゃないんですか?」
「半分正解。でも面接の時間は30〜60分しかない。実際の設計に何週間もかかるシステムを、その時間で評価しようとしている。つまり面接官は完璧な設計を求めていない。」
「じゃあ、何を見てるんですか。」
「あなたが一緒に働けるエンジニアかどうか。」
レイカはホワイトボードに書いた。
面接官が評価しているもの
━━━━━━━━━━━━━━━━━━━
1. 要件の整理力
→ 「何を作るか」を曖昧なままにしない
2. 数字への感覚
→ スケール感を定量的に語れるか
3. 設計の引き出し
→ 適切なコンポーネントを選択できるか
4. トレードオフの言語化
→ なぜそれを選んだか説明できるか
5. コミュニケーション
→ 考えながら話せるか、議論できるか
「Amazonでは Bar Raiser というプロセスがある。面接の中で意図的に難しい質問を投げて、候補者がどう対応するかを見る。正解を求めているんじゃなくて、プレッシャー下での思考プロセスを見ている。」
「Googleも同じ。採用基準の一つに『Googleyness』という言葉がある。あいまいな問題を前にして、構造化して考えられるか。チームで議論できるか。それを面接で評価する。」
INFO
Amazon の Bar Raiser は、採用マネージャーの偏りを排除するための独立した評価者制度。「この人を採用したら組織の基準が上がるか?」を判断する。システム設計面接では、候補者が「一緒に設計会議をしたいと思える人か」を見ている。
「ソウタが面接で失敗したのは、設計力がないからじゃない。フレームワークがなかったからだ。問題を見た瞬間に何から始めればいいか、どういう順番で話せばいいか、それが頭にインストールされていなかった。」
よくある誤解 TOP 5
「次に」とレイカは続けた。「システム設計面接についての誤解を整理しておく。」
誤解1:正解を出すことが目的だ
「違う。30〜60分で完璧なシステムを設計できる人間はいない。面接官もそれを知っている。プロセスを見ている。」
誤解2:知識があれば解ける
「ソウタは言ってた、『Redis使えばいいのかな』って。でも言語化できなかった。知識と、それを使って設計する力は別物だ。」
誤解3:一人で黙々と考えて完成品を出すべきだ
「システム設計面接はコラボレーションだ。黙って考えていたら、面接官には何も伝わらない。考えながら声に出す。これが基本中の基本。」
誤解4:技術的な詳細を詰めるべきだ
「高レベルの設計が固まる前に、データベースのインデックス設計を始めてはいけない。面接の時間は限られている。優先順位がある。」
誤解5:実務経験があれば有利だ
ソウタはドキッとした。
「実務と面接は違う。実務ではチームがあって、時間があって、実際に動かしながら設計を修正できる。面接ではそれができない。だから別の準備が必要だ。」
WARNING
「実務でRailsを書いているからシステム設計面接は大丈夫」という考えは危険。面接で求められる「スケールを想定した設計思考」は、日常業務では意識しない視点だ。意図的に練習しないと身につかない。
この1ヶ月の学習プラン
「具体的に何を学べばいいんですか」とソウタは聞いた。
「4週間で、順番に積み上げる。」
「1週目:フレームワーク。システム設計面接の4ステップを叩き込む。要件確認、見積もり、高レベル設計、詳細設計。どんな問題が出ても、この順番で進める。」
「2週目:見積もり力。数字で考える練習をする。1秒あたりのリクエスト数、必要なストレージ量、帯域幅。これが計算できないと、設計の根拠が語れない。」
「3週目:実践問題。URL短縮サービス、SNSのタイムライン、チャットシステム、動画配信サービス。典型的な問題を繰り返し解く。」
「4週目:模擬面接。私が面接官役をやる。本番と同じプレッシャーの中で話す練習をする。」
ソウタはノートに書き写した。
「Railsを使って、手を動かしながら学べますか?」
「もちろん。概念だけじゃなく、実際に書けることが重要だ。各週でRailsのコードを書く。」
たとえば1週目に学ぶフレームワークの最初のステップ、「要件確認」をRailsのモデル設計で考えるとこんなイメージだ。
# システム設計面接での要件確認を、Railsのモデル設計で考えると…
# まず「何を作るか」を明確にする
# 例:Twitterのタイムラインサービスの要件確認
# Q: DAUは? → 1000万人
# Q: 書き込みと読み込みの比率は? → 1:100(読み込みが多い)
# Q: 1ユーザーの平均フォロー数は? → 200人
# Q: リアルタイム性は? → 数秒の遅延は許容
# 要件が決まってから、初めてテーブル設計を考える
class CreateTweets < ActiveRecord::Migration[7.1]
def change
create_table :tweets do |t|
t.references :user, null: false, foreign_key: true
t.text :content, null: false
t.integer :retweet_count, default: 0
t.integer :like_count, default: 0
t.timestamps
end
# 読み込みが多い → 作成時刻での降順取得を高速化
add_index :tweets, [:user_id, :created_at]
end
end「要件を確認することで、インデックスの設計方針も決まる。読み込み重視なら、書き込みが少し遅くなってもインデックスを積極的に張る。これがトレードオフを考えるということだ。」
INFO
Railsで実装するとき、最初からadd_indexをどこに張るか考えることが、システム設計思考の第一歩。「なんとなくuser_idにインデックスを張る」ではなく、「どのクエリが多いか」を考えてインデックスを設計する習慣をつけよう。
学習の全体構造
「この本の構成はそのまま1ヶ月の学習プランに対応している。毎日1章ずつ読んで、手を動かして、次の章に進む。」
「全部読み終わったとき、あなたの頭にシステム設計のOSがインストールされている。設計の知識じゃなくて、考え方のフレームワークを。」
レイカの言葉に、ソウタはノートを開いた。
面接官の視点——あの30分間を振り返ると
後日、PayScaleの面接で出会ったCTO補佐がエンジニアのコミュニティで書いたブログ記事をソウタは偶然読んだ。
「システム設計面接で落とした候補者へ」というタイトルだった。
先日、ある優秀な候補者を不合格にしました。コーディングは完璧でした。実務経験も豊富でした。でも、システム設計の問題を出したとき、彼は「1億ユーザーという前提」を聞いて完全に止まりました。
私が見たかったのは、完璧な設計ではありませんでした。「1億ユーザーなら、1秒あたり何リクエストになりますか?」と逆に聞いてほしかった。「まず要件を確認させてください」と言ってほしかった。
面接は会話です。答えを出す場所ではなく、一緒に考える場所です。
ソウタはその記事を3回読んだ。
「一緒に考える場所。」
そのひとことが、ソウタの中で何かを変えた。
キャッシュ戦略——設計の選択肢と根拠
「もうひとつ、今日は具体的なコードを見せておく」とレイカが言った。
「システム設計でよく出てくるのがキャッシュ戦略だ。Railsでの実装方法はいくつかある。どれが正解かは、要件による。」
# キャッシュ戦略の選択肢(正解は文脈による)
# Option 1: Read-Through Cache(シンプルで使いやすい)
class TimelineService
def fetch(user_id)
Rails.cache.fetch("timeline:#{user_id}", expires_in: 1.minute) do
build_timeline(user_id)
end
end
private
def build_timeline(user_id)
user = User.find(user_id)
Tweet
.joins(:user)
.where(user: user.following)
.order(created_at: :desc)
.limit(20)
end
end
# Option 2: Write-Through Cache(リアルタイム性が高い)
class Tweet < ApplicationRecord
after_create :invalidate_follower_timelines
private
def invalidate_follower_timelines
user.followers.each do |follower|
Rails.cache.delete("timeline:#{follower.id}")
end
end
end「Option 1はシンプルだが、キャッシュが切れたタイミングに全ユーザーがDBにアクセスするThunddering Herd問題が起きる。Option 2はリアルタイム性が高いが、フォロワーが多いユーザー(例:芸能人)が1ツイートするたびに大量のキャッシュ削除が走る。」
「どちらが正解か、ではなくて。どちらを選ぶか、その根拠を説明できること。それが面接で求められることだ。」
# Option 3: Pre-computed Timeline(大規模向け、非同期処理)
class TimelineWorker
include Sidekiq::Worker
def perform(tweet_id)
tweet = Tweet.find(tweet_id)
# フォロワーのタイムラインを事前計算してElastiCacheに格納
tweet.user.followers.find_each do |follower|
timeline_key = "timeline:#{follower.id}"
# AWS ElastiCache(Redis)にPush
$redis.lpush(timeline_key, tweet.id)
$redis.ltrim(timeline_key, 0, 999) # 最新1000件だけ保持
$redis.expire(timeline_key, 86400) # 24時間TTL
end
end
end「Option 3はTwitterが実際に採用しているPush型のアプローチに近い。非同期処理でタイムラインを事前生成する。読み込みは超高速だが、書き込みコストが高い。」
ソウタはコードを見ながらノートに書いた。
「設計の選択肢を3つ挙げて、トレードオフを比較して、要件に合ったものを選ぶ。そういうことですか。」
「そう。それだけでいい。面接は完璧な答えを出す場所じゃなくて、考え方を見せる場所だから。」
始める前に知っておくべきこと
「最後に」とレイカは言った。「システム設計は暗記科目ではない。」
「正しい設計が唯一あるわけではないし、完璧な答えを出せる人間など存在しない。AmazonとGoogleですら、同じ問題に対して異なるアーキテクチャを採用している。」
大切なのはこの4つだ。
- 問題をきちんと分解できること
- 選択の根拠を説明できること
- スケールを意識した議論ができること
- 面接官と対話しながら設計できること
INFO
コードを書ける力と、システムを設計できる力は別物だ。でもRailsを日常的に書いているなら、設計の選択肢をコードで示せるという強みがある。この本では、設計の概念をRailsのコードと対応させながら学んでいく。
ソウタのノート——1ヶ月のスタートラインに立つ前に
レイカのオフィスからの帰り道、ソウタは電車の中でノートに書いた。
今日学んだこと
1. 面接は「正解を出す場所」じゃなく「一緒に考える場所」
2. 要件を確認することが全ての始まり
3. 数字で考える習慣をつける
4. フレームワーク(順番)を持つことで迷子にならない
5. Railsの実装力を「設計思考」と結びつける
次回までにやること
・1秒あたりのリクエスト数の計算練習
・「Twitterのタイムライン設計」の要件確認を10個考える
・キャッシュ戦略のトレードオフを自分の言葉でまとめる
「じゃあ、始めましょう。まず最初は——フレームワークの話。あなたが面接で迷子にならないための地図を渡す。」
レイカはマーカーを手に取り、ホワイトボードの前に立った。
その瞬間から、ソウタの1ヶ月が始まった。