システム設計面接でも設計ドキュメントでも、考え方が目に見える形になるのは図です。「キャッシュ」と書いた箱を描くのは簡単ですが、図を読む価値のあるものにするのは、なぜそこにあるのか、どれだけのトラフィックを吸収するのか、キャッシュが冷えているときに何が起きるのかを示すことです。
このガイドでは、定番の題材である URL 短縮サービスを、要件から完成したシステム設計図まで順に見ていきます。大事なのは短縮サービスそのものではなく手順です。まず要件と数字、次に API とデータ、そのあとで箱を描き、それぞれの判断を誰もが見える場所に書き込みます。
ステップ 1: 要件
機能要件
- 長い URL から短いリンクを作成します。カスタムエイリアスと有効期限は任意で指定できます。
- 短いリンクを元の長い URL にリダイレクトします。
- リンクごとのクリック数を数えます。
非機能要件
- リダイレクトは高速かつ高可用である必要があります。リンクの作成は多少の遅延を許容できます。
- 読み取り中心のシステムです。リンクは一度作成され、何度もクリックされます。
- 短縮コードは簡単に列挙できないようにします。
ステップ 2: ざっくり見積もる
新規リンクを月に 1 億件、作成されたリンクごとのリダイレクトを 100 回と仮定します。
| 項目 | 見積もり |
|---|---|
| 書き込み | 100M / 月あたり 2.6M 秒 ≈ 平均で毎秒 40 件 |
| 読み取り | 100 × 40 ≈ 毎秒 4,000 リダイレクト。ピークは 10× を見込み、毎秒 40,000 件 |
| 5 年後のリンク数 | 100M × 12 × 5 = 60 億 |
| ストレージ | 60 億 × ~500 バイト ≈ 3 TB(レプリカを除く) |
| コード長 | base62 で 7 文字なら 62⁷ ≈ 3.5 兆通りのコード |
この時点で、数字がすでに設計を形づくっています。読み取りが大半なので、読み取り経路にはキャッシュを置きます。3 TB は小さなデータベース 1 台にいつまでも収まるわけではないので、データモデルはきれいにシャーディングできる必要があります。そして 7 文字あれば余裕は十分です。
ステップ 3: API
POST /api/links
{ "url": "https://example.com/a/very/long/path", "alias": "launch", "expiresAt": "2027-01-01" }
→ 201 { "code": "launch", "shortUrl": "https://s.example/launch" }
GET /{code}
→ 302 Location: https://example.com/a/very/long/path
図に一言書き添えておくべき判断があります。301 か 302 か? 301 は恒久的なリダイレクトなので、ブラウザがキャッシュし、同じリンクへの再訪問はこちらに届きません。トラフィックは節約できますが、クリック数を失います。302 ならすべてのクリックがリダイレクトサービスを通るので、分析に必要なデータが取れます。この設計では 302 を使います。
ステップ 4: データモデルと ID
中心となるテーブルはシンプルで、code(主キー)、long_url、owner_id、created_at、expires_at で構成されます。読み取りはすべて code による検索なので、キーバリューストアでも、主キーを持つリレーショナルデータベースでも対応できます。code のハッシュでシャーディングすれば、負荷は均等に分散します。
面白いのはコードの生成方法です。
| 方式 | 長所 | 短所 |
|---|---|---|
| カウンター + base62 | 衝突しない | 連番のコードは列挙しやすい。協調が必要 |
| ランダムな 7 文字 | 推測されにくく、協調も不要 | 挿入時に衝突チェックが必要 |
| URL のハッシュ | 同じ URL には同じコード | 切り詰めると衝突が起きる。エイリアスは別扱いが必要 |
ここでは、書き込み API が ID レンジアロケーター から ID の範囲を(たとえば 100 万件ずつ)リースし、base62 でエンコードする前に各 ID を可逆な置換でかき混ぜます。これにより、衝突チェックなしでもコードは一意になり、外から見ても連番には見えません。
ステップ 5: 図にする
要件の異なる経路は分けて描きます。書き込み経路は量が少なく、正確でなければなりません。読み取り経路は量が多く、速くなければなりません。分析は遅れてもかまいません。
ダイアグラムの内容
- Users
- Load balancer
- ID range allocator
- Write API
- Redirect service
- Cache: code → URL
- Links DB (sharded by code)
- click events
- Analytics workers
- Analytics store
- HTTPS
- POST · 40/s
- GET · 4k/s, peak 40k/s
- lease ID range
- INSERT link
- GET code
- on cache miss
- click event
- consume
- counts
図の読み方は次のとおりです。
- 書き込み経路(上段)。 ロードバランサーが
POST /api/linksを書き込み API に振り分けます。書き込み API はリース済みの範囲から ID を取り出してエンコードし、リンクをデータベースに書き込みます。 - 読み取り経路(中段)。
GET /{code}はリダイレクトサービスに届き、まず Redis を確認します。よくアクセスされるデータがメモリに収まっていれば、ほとんどのリダイレクトはデータベースに触れません。キャッシュミスのときはデータベースを読み、キャッシュを埋めます。 - 分析(下段)。 リダイレクトのたびにクリックイベントが Kafka に送られ、分析ワーカーが件数を集計して分析ストアに書き込みます。リダイレクトが分析を待つことはありません。
判断の理由が見えるように描く
- エッジに数字を書く。 書き込みのエッジに
40/s、読み取りのエッジに4k/s, peak 40k/sと書けば、どんなラベルよりもキャッシュが必要な理由が伝わります。 - 箱の横に前提を書く。 「キャッシュヒット率の目標は 90%」「分析のために 302」「ID は 1M 単位でリース」などです。
- 同期と非同期を分ける。 リクエストの経路は実線、クリックイベントは破線にします。
- 障害時に何が起きるかを示す。 Redis が落ちたら、リダイレクトはレイテンシーが上がった状態でデータベースにフォールバックします。そのことを図に書いておきます。
- 見積もりをキャンバスに残す。 図の横に数字の小さな表を置けば、図がそのまま設計ドキュメントになります。
準備しておきたい深掘りポイント
- キャッシュ。 人気のリンクはロングテール分布に従うため、TTL を控えめにした LRU キャッシュでトラフィックの大半をカバーできます。削除時と期限切れ時に無効化します。
- データベースのスケーリング。 ここではリードレプリカはキャッシュほど効きません。コードでシャーディングすれば、書き込みとストレージが均等に増えていきます。
- 有効期限。 巨大な削除ジョブを回すのではなく、読み取り時に
expires_atを確認し、バックグラウンドで掃除します。 - 不正利用。 ユーザーごとにリンク作成のレート制限をかけ、リンク先 URL をマルウェアのリストと照合します。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- ユーザー用にネットワークパックの インターネット ノードを、ロードバランサー用にクラウドパックの クラウドロードバランサー を追加します。
- 書き込み API、リダイレクトサービス、ID アロケーター、分析ワーカーを C4 モデルパックの コンテナ ノードとして追加し、「技術」フィールドに言語を入力します。
- データパックから、キャッシュ用に Redis、リンクのデータベース用に Postgres、クリックイベント用に トピック、分析用に データウェアハウス を追加します。
- エッジに操作とレートのラベルを付け、クリックイベントは破線にして、見積もりを基本パックの Markdown ブロックに書きます。
この例で使うダイアグラムノードは、見積もりを書いた Markdown ブロックを含めて 11 個で、Free プランの範囲に収まります。あらゆるアーキテクチャ図に共通する基本的な手順は、ソフトウェアアーキテクチャ図の描き方を参照してください。
よくある質問
面接では、システム設計図をどこまで詳しく描けばよいですか? 読み取りと書き込みの経路、データストア、キャッシュ、そして非同期の要素を 1 つ示せれば十分です。深掘りするのは、面接官に聞かれたところだけにしましょう。
リンクのテーブルは SQL と NoSQL のどちらにすべきですか? 主キーでの検索なら、どちらでも対応できます。どうシャーディングするつもりか、チームが何をうまく運用できるかで選び、その理由を図に書いておきます。
CDN は描くべきですか? リダイレクトをエッジでキャッシュする場合だけです。302 とクリック計測を前提にするとエッジキャッシュの効果は限られるため、多くの設計では省きます。
なぜすべて 301 にしないのですか? 301 はトラフィックを節約できますが、ブラウザが問い合わせてこなくなるため、クリックのデータを失い、リピーターに対してはリンク先も変更できなくなります。
数字はどこに書きますか? キャンバス上の、図の横です。そうすれば設計をレビューする人なら誰でも、箱の背後にある前提を確認できます。