0Pricing
Coding Interview Prep · レッスン

システム設計面接のフレームワーク

5段階のRADIOフレームワーク(Requirements、API、Data、Infrastructure、Optimise)を確認し、URL短縮サービスへの適用を練習します。

「システム設計面接のフレームワーク」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

面接でシステム設計が重要な理由

システム設計の面接では、大規模な環境で考える能力が試されます。たとえば、数十億人のユーザーを対象に Twitter、YouTube、または URL 短縮サービスをどのように設計するかを考えます。1つの正解があるコーディング問題とは異なり、システム設計は自由度が高く、トレードオフを判断してその理由を説明する必要があります。シニアやスタッフレベルの職種では、この面接だけで30〜45分を使うこともあります。

面接官は、要件を明確にし、負荷を見積もり、高レベルのアーキテクチャを提案し、主要なコンポーネントを詳しく検討し、トレードオフを議論できるかどうかを評価します。それらすべてを、明確にコミュニケーションしながら行わなければなりません。構造化されたフレームワークを使うと、話が脱線するのを防ぎ、重要な観点をすべて扱えるようになります。

# System design is not about a single correct answer.
# Interviewers look for:
eval_criteria = [
    'Ability to clarify requirements before designing',
    'Back-of-envelope capacity estimation',
    'High-level architecture with clear components',
    'Data modelling and storage choice',
    'Handling scalability (10x, 100x load)',
    'Trade-off discussion (consistency vs availability, etc.)',
    'Communication: talking through decisions as you make them',
]
for c in eval_criteria:
    print('-', c)

RADIOフレームワークの概要

RADIOフレームワークは、あらゆるシステム設計の面接で繰り返し使える5段階の構成を提供します。

  • R — 要件:機能要件と非機能要件
  • A — API設計:システムはどのような操作を公開するか
  • D — データモデル:どのようなデータをどのように保存するか
  • I — インフラストラクチャ:高レベルのコンポーネント(サーバー、キュー、キャッシュ)
  • O — 最適化:ボトルネック、キャッシュ、シャーディング、レプリケーション

常にこの順番で進めますが、新しい知見が得られたら前の段階に戻って内容を調整してください。各段階にはおおむね同じ時間をかけます。要件を明確にする前に、いきなり箱を描き始めてはいけません。

RADIO = {
    'R': 'Requirements — What must the system do? What scale?',
    'A': 'API         — Define endpoints/operations the system exposes',
    'D': 'Data Model  — Entities, schemas, storage types',
    'I': 'Infra       — High-level architecture: servers, queues, caches, CDN',
    'O': 'Optimise    — Identify and address bottlenecks, trade-offs',
}
for step, desc in RADIO.items():
    print(f'[{step}] {desc}')

print('\nTiming guide for a 45-min interview:')
print('  R: 5 min | A: 5 min | D: 10 min | I: 15 min | O: 10 min')

ステップR:要件を明確にする

要件を明確にせずに設計を始めてはいけません。機能要件(システムが何をするか)と非機能要件(規模、レイテンシ、可用性)について質問します。URL短縮サービスの場合は、次のようになります。

  • 機能要件:URLを短縮する、元のURLへリダイレクトする、必要に応じてカスタムエイリアスと有効期限をサポートする
  • 非機能要件:1日に何件のURLを扱うか。読み取り中心か書き込み中心か。可用性の要件は 99.9% か 99.99% か。許容できるレイテンシはどの程度か

前提を明示すると、成熟した考え方を示せます。面接官は、適切な質問ができるかを見るために、意図的に曖昧な仕様を提示することがよくあります。2分間要件を確認するだけで、間違ったシステムを設計することを防げます。

# Requirements questions for URL Shortener:
functional = [
    'Shorten a given URL to a 7-character alias',
    'Redirect short URL to original URL',
    'Allow custom aliases (optional)',
    'URL expiry (optional)',
    'Analytics: click count per URL (optional)',
]
non_functional = [
    '100 million new URLs per day (write: ~1160/sec)',
    '10:1 read:write ratio => 11,600 redirects/sec',
    'Redirects must be < 100ms p99 latency',
    '99.99% availability (< 1 hr downtime/year)',
    'URLs must be globally accessible',
]
print('Functional:')
for f in functional: print(' -', f)
print('\nNon-functional:')
for nf in non_functional: print(' -', nf)

ステップR:概算見積もり

要件を確認したら、キャパシティを見積もります。これにより、解決策を提案する前に規模について考えられることを示せます。導き出す主な数値は、1秒あたりのリクエスト数(RPS)、1日または1年に必要なストレージ容量、帯域幅、そしてキャッシュに必要なメモリです。

丸めた数値を使い、自由に概算してください。面接官が重視するのは正確な数値ではなく、桁の大きさです。URL短縮サービスの例では、100M writes/day ÷ 86400 ≈ 1160 writes/sec、10B reads/day ÷ 86400 ≈ 115K reads/sec となります。1つのURLレコードを約500バイトとすると、100M × 500B = 50 GB/day、18 TB/year です。

# Back-of-envelope for URL Shortener
writes_per_day = 100_000_000        # 100 million URLs/day
read_write_ratio = 100              # 100:1 read/write
reads_per_day = writes_per_day * read_write_ratio
bytes_per_url = 500                 # url string + metadata
years_to_store = 5

print('=== Capacity Estimation ===')
print(f'Writes/sec:  {writes_per_day / 86400:.0f}')
print(f'Reads/sec:   {reads_per_day / 86400:,.0f}')
print(f'Storage/day: {writes_per_day * bytes_per_url / 1e9:.1f} GB')
print(f'Storage total ({years_to_store}y): {writes_per_day * bytes_per_url * 365 * years_to_store / 1e12:.1f} TB')

cache_hit_rate = 0.80
hot_urls = reads_per_day * (1 - cache_hit_rate)
print(f'\n80% cache hit rate: {cache_hit_rate*100}% of reads from cache')
print(f'DB reads/sec: {hot_urls / 86400:,.0f}')

ステップA:API設計

APIの公開範囲、つまりシステムがクライアントや内部サービスに公開する操作を定義します。HTTPメソッド、エンドポイントのパス、リクエストパラメータ、レスポンス形式を明確に指定してください。これが設計の土台になります。その他のすべては、これらのAPIを実装するために存在します。

URL短縮サービスの中核となるAPIは、次の2つです。(1) 短縮URLを作成する POST /shorten、(2) リダイレクトする GET /{alias}。オプションとして、削除用の DELETE /{alias} や、分析用の GET /{alias}/stats もあります。レスポンスコード(201 Created、301 Redirect、404 Not Found)も指定します。

# API Design for URL Shortener
apis = [
    {
        'method': 'POST',
        'path': '/api/v1/shorten',
        'request': '{"long_url": "https://...", "alias": "optional", "expires_at": "optional"}',
        'response': '201 Created: {"short_url": "https://short.ly/abc1234", "alias": "abc1234"}',
    },
    {
        'method': 'GET',
        'path': '/{alias}',
        'request': 'No body',
        'response': '301 Redirect to long_url (or 404 Not Found)',
    },
    {
        'method': 'GET',
        'path': '/api/v1/{alias}/stats',
        'request': 'Optional: date range query params',
        'response': '200 OK: {"clicks": 42000, "unique_visitors": 15000}',
    },
]
for api in apis:
    print(f'{api["method"]} {api["path"]}')
    print(f'  Request:  {api["request"]}')
    print(f'  Response: {api["response"]}')
    print()

ステップD:データモデル

データモデルでは、何をどのように保存するかを定義します。中核となるエンティティと、その属性を特定してください。URL短縮サービスの場合、alias(主キー)、long_url、created_at、expires_at、user_id を持つ urls テーブルが考えられます。分析用に clicks テーブルを追加してもよいでしょう。

適切なストレージの種類を選ぶことが重要です。構造化されたデータや複雑なクエリにはリレーショナルDB、大規模な O(1) のエイリアス検索にはキーバリューストア(Redis、DynamoDB)、大きなデータの塊にはオブジェクトストア(S3)を使います。URL短縮サービスでは、読み取りにはエイリアスをキーとするキーバリューストアが理想的で、書き込みと管理にはリレーショナルDBを使います。

# Data model for URL Shortener

# Core table (PostgreSQL)
urls_schema = '''
CREATE TABLE urls (
    alias       VARCHAR(16) PRIMARY KEY,   -- e.g., 'abc1234'
    long_url    TEXT NOT NULL,
    user_id     UUID REFERENCES users(id),
    created_at  TIMESTAMP DEFAULT NOW(),
    expires_at  TIMESTAMP,
    click_count BIGINT DEFAULT 0
);
CREATE INDEX ON urls(user_id);
'''

# Cache layer (Redis) for hot reads
redis_schema = '''
alias  =>  long_url       # O(1) GET on cache hit
TTL = 24 hours (or until expiry)
Cache eviction: LRU
'''

print('PostgreSQL schema:')
print(urls_schema)
print('Redis cache:')
print(redis_schema)
print('Storage split: Redis for hot reads (~80%), PostgreSQL for writes and cold reads')

ステップI:高レベルのインフラストラクチャ

高レベルのインフラストラクチャを描きます。どのサーバーがどの責務を担当するか、コンポーネント間でデータがどのように流れるか、どの外部サービスを利用するかを示します。大規模なURL短縮サービスでは、次のようになります。

  • ロードバランサー:トラフィックを読み取りサービスと書き込みサービスのレプリカに分散する
  • 書き込みサービス:エイリアスを生成し、一意性を検証してDBに書き込み、キャッシュを無効化する
  • 読み取り/リダイレクトサービス:まずRedisキャッシュを確認し、キャッシュミス時はDBにフォールバックする
  • リレーショナルDB:信頼できる唯一の情報源(リードレプリカ付き)
  • Redisクラスター:頻繁にアクセスされるURLのマッピングをキャッシュし、1ミリ秒未満で読み取れるようにする
# ASCII architecture sketch
architecture = '''
         Clients
            |
       [Load Balancer]
       /             \\
  [Write API]    [Read/Redirect API]
      |                  |  |
  [Alias Gen]       [Redis Cache]
      |                  |
  [PostgreSQL]---->[Read Replicas]

Alias Generation:
- Base62 encoding of auto-incrementing ID
- 62^7 = 3.5 trillion unique URLs (enough for 5 years at 100M/day)
- OR random 7-char Base62 with collision check
'''
print(architecture)
print('Key design decisions:')
print('  - Read/Write split: separate services for scalability')
print('  - Cache-aside pattern: read from Redis, fallback to DB')
print('  - 301 vs 302 redirect: 301 cached by browser (less load), 302 always hits server (analytics)')

ステップO:最適化とボトルネックへの対処

最適化の段階では、ボトルネックに対処してシステムをスケールさせます。URL短縮サービスで重要な懸念事項は、リダイレクトのレイテンシ(CDNやエッジキャッシュを使ってRedisをユーザーの近くに配置する)、大規模環境でのエイリアスの一意性(中央チケットサーバーを使う、または衝突検出付きのハッシュを使う)、DBの書き込みボトルネック(キューを使ったバッチ書き込みや非同期書き込み)です。

トレードオフを明確に議論してください。301リダイレクトはサーバーの負荷を減らしますが、分析の正確性が失われます。302リダイレクトは追跡できますが、レイテンシが増加します。有効期限の長いキャッシュはDBの負荷を減らしますが、古いURLが残るリスクがあります。こうした緊張関係を理解していることを示せれば、シニアレベルの思考力を示せます。

# Key optimisation discussion points
optimisations = {
    'Read latency': [
        'Redis cluster in multiple regions (CDN-edge caching)',
        '301 redirect for non-tracked URLs (client caches)',
        '302 redirect when click analytics are needed',
    ],
    'Write throughput': [
        'Async writes: accept request, enqueue to Kafka, batch-commit to DB',
        'Ticket server (centralised ID generator) to avoid UUID collision',
        'Or: hash(user_id + timestamp + random) with retry on collision',
    ],
    'Availability': [
        'Multi-AZ PostgreSQL with automatic failover',
        'Redis Sentinel or Redis Cluster for HA cache',
        'Health checks + circuit breaker on each service',
    ],
    'Storage': [
        'Partition urls table by hash(alias) for horizontal scaling',
        'Archive expired URLs to cold storage (S3)',
    ],
}
for area, points in optimisations.items():
    print(f'{area}:')
    for p in points: print(f'  - {p}')
    print()

エイリアスの生成:Base62エンコーディング

URL短縮サービスにおける中核的な技術上の詳細は、短く一意なエイリアスをどのように生成するかです。標準的な方法は、データベースから取得した自動インクリメント整数IDをBase62(数字 0-9、英字 a-z、A-Z)でエンコードすることです。7文字のBase62文字列では 62^7 ≈ 3.5 trillion 個の一意なURLを表現できるため、1日1億件のペースでも数十年分に相当します。

この方法は各IDが一意であるため衝突がなく、短くURLで安全に使える文字列を生成できます。ID→alias のマッピングは決定的で、可逆でもあります。一方で、連番のIDから予測可能なエイリアスが生成されるというトレードオフがあります(セキュリティ上の懸念です)。Base62のアルファベットをシャッフルするか、カウンターにオフセットを加えて予測しにくくします。

import string

BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
BASE = 62

def encode_base62(num):
    if num == 0:
        return BASE62_CHARS[0]
    result = ''
    while num:
        result = BASE62_CHARS[num % BASE] + result
        num //= BASE
    return result

def decode_base62(s):
    result = 0
    for c in s:
        result = result * BASE + BASE62_CHARS.index(c)
    return result

# Generate aliases for IDs 1, 100, 1000, 10^9
for id_ in [1, 100, 1000, 1_000_000, 10**9, 3_521_614_606207]:
    alias = encode_base62(id_)
    decoded = decode_base62(alias)
    print(f'ID {id_:20,} => alias "{alias}" (len={len(alias)}) => decoded={decoded}')

Twitterのフィード設計にRADIOを適用する

フレームワークが一般化できることを示すため、Twitterのようなニュースフィードの設計にRADIOを簡単に適用してみましょう。

  • R:ユーザーはツイートを投稿し、他のユーザーをフォローし、フォローしているユーザーのツイートで構成されたフィードを閲覧します。規模はユーザー3億人、1日5億ツイートで、フィードは <2s で読み込まれる必要があります。
  • A:POST /tweets、GET /feed、GET /timeline/{user_id}
  • D:tweets テーブル(id、user_id、content、created_at)、follows テーブル(follower_id、followee_id)、ユーザーごとのフィードキャッシュ
  • I:ファンアウトサービスが新しいツイートをフォロワーのフィードに書き込む(事前計算)。書き込みの多いフォロー/ツイートテーブルにはCassandra、フィードキャッシュにはRedisを使います
# Fan-out on Write vs Fan-out on Read trade-off
fan_out_strategies = {
    'Fan-out on Write (Push)': {
        'How': 'When user A posts, immediately write to all followers feeds',
        'Pro': 'O(1) feed read — feed is precomputed',
        'Con': 'Celebrities with 10M followers => 10M writes per post; very slow write',
        'Best for': 'Users with few followers (regular users)',
    },
    'Fan-out on Read (Pull)': {
        'How': 'When user reads feed, query followees tweets and merge',
        'Pro': 'Writes are fast (one DB write per tweet)',
        'Con': 'Feed read is slow: must query all N followees',
        'Best for': 'Celebrity accounts (few reads per post)',
    },
    'Hybrid': {
        'How': 'Push for regular users, pull for celebrities',
        'Pro': 'Balances read and write cost',
        'Con': 'More complex implementation',
        'Best for': 'Production systems like Twitter',
    },
}
for strategy, details in fan_out_strategies.items():
    print(f'{strategy}:')
    for k, v in details.items(): print(f'  {k}: {v}')
    print()

システム設計面接でよくある間違い

システム設計面接を台無しにしてしまう、よくある間違いを避けてください。

  • すぐに解決策へ飛びつく:要件を明確にする前に箱を描き始めると、エンジニアとしての習慣が身についていないと見なされます
  • 見積もりをしない:規模を把握せずに設計するのは、当てずっぽうにすぎません
  • 過剰設計:設問が1万人を想定しているのに、10億ユーザー向けに設計すると、面接時間を無駄にします
  • トレードオフを説明しない:どの選択にも長所と短所があります。それに触れないと、理解が浅いと思われます
  • 黙ってしまう:面接官は思考過程を聞く必要があります。意思決定を行う際は、その内容を説明してください
# Checklist: before you stop talking, verify you covered:
checklist = [
    '[ ] Asked clarifying questions about scale and constraints',
    '[ ] Made capacity estimates (RPS, storage, bandwidth)',
    '[ ] Defined the API surface clearly',
    '[ ] Described the data model and storage choices',
    '[ ] Drew a high-level architecture with named components',
    '[ ] Identified the main bottleneck and proposed a solution',
    '[ ] Discussed at least one trade-off explicitly',
    '[ ] Verified the design meets the stated requirements',
]
for item in checklist:
    print(item)

理解度チェック

このレッスンで学んだ Data Structures & Algorithms — Coding Interview Prep の概念を理解できているか確認しましょう。

レッスンのまとめ

このレッスンでは、RADIOフレームワークによって、システム設計面接をRequirements(要件)、API、Data Model(データモデル)、Infrastructure(インフラストラクチャ)、Optimise(最適化)に沿って整理できること、設計を提案する前に、機能要件と非機能要件を必ず明確にし、キャパシティを見積もること、そしてトレードオフを明示的に説明すること――すべての設計上の選択には長所と短所があり、面接官はそれを説明することを期待していることを学びました。次は、アクセスパターン、整合性、規模に基づいて、SQLとNoSQLのストレージエンジンをどのように選ぶかを見ていきます。

よくある質問

「システム設計面接のフレームワーク」レッスンは無料ですか?

はい。「システム設計面接のフレームワーク」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「システム設計面接のフレームワーク」で何を学びますか?

5段階のRADIOフレームワーク(Requirements、API、Data、Infrastructure、Optimise)を確認し、URL短縮サービスへの適用を練習します。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「システム設計面接のフレームワーク」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. システム設計面接のフレームワーク
  2. スケーラブルなデータストレージ:SQLとNoSQL
  3. キャッシュ、CDN、ロードバランシング
  4. Rate Limiterの設計とTwitterフィードの設計
← Coding Interview Prepに戻る