🐳

外資系企業の「システムデザイン面接」を突破するために、本気でURL短縮サービスを作ってみた

に公開

「URL短縮サービス(Design a URL Shortener)」といえば、GoogleやMetaなど、トップテック企業のシステム設計面接(System Design Interview)で頻出の定番問題です。

「ID生成はどうする?」「ハッシュ衝突の対策は?」「キャッシュ戦略は?」

面接対策としてこれらを机上で議論することはあっても、「実際にコードに落とし込み、正しく動作することを証明した」 経験がある人は意外と少ないのではないでしょうか?

今回は、この定番のお題に対して、「スケーラビリティ」「堅牢性」そして「テスト」 をテーマに、Go言語でフルスクラッチ実装してみました。

特に、結合テストにおいては Testcontainers for Go を採用し、docker-compose に依存しないモダンなテスト環境の構築に挑戦しています。

1. なぜ今、「URL短縮」なのか?

システム設計の勘所を実践形式で学ぶ上で、非常に優れた題材だと感じています。

  1. Read-Heavyな特性: 読み込みが書き込みより圧倒的に多い(100:1など)ため、キャッシュ戦略が不可欠。
  2. 一意性の保証: 短縮IDが重複するとサービスとして破綻するため、並行処理への理解が問われる。
  3. データ整合性: キャッシュとDBの不整合をどう防ぐか。

単に動くものを作るだけでなく、「なぜその技術を選ぶのか」 という設計判断のプロセスを重視して実装しました。

2. アーキテクチャと設計判断

① ID生成:ハッシュ関数 vs Base62エンコーディング

短縮URL(例: abc12)をどう生成するかは最大の論点です。MD5などのハッシュ関数を使う手法もありますが、Base62エンコーディング を採用しました。

  • ハッシュ関数 (MD5/SHA): 生成結果が長すぎるため切り詰め(Truncate)が必要。結果として衝突(Collision) が発生する可能性があり、その都度DBへの存在確認が必要になる(書き込み性能の低下)。
  • Base62 + Auto Increment: DBのユニークID(数値)を62進数(a-z, A-Z, 0-9)に変換する。数学的に一対一対応(Bijective) するため、衝突チェックが不要で、計算量 O(1) で生成可能。

https://github.com/hszk-dev/url-shortener/blob/c0161942ab09bcbd54ceccc2a7cbe7a339d12371/internal/shortener/base62.go#L13-L25

② HTTPステータス:301 vs 302

リダイレクトには 301 Moved Permanently を使うのが一般的ですが、今回はあえて 302 Found を採用しました。

  • 301: ブラウザがキャッシュするためサーバー負荷は下がるが、2回目以降のアクセスがサーバーに届かず、分析(Analytics)ができない
  • 302: 毎回サーバーを経由するため、クリック数やユーザー属性のトラッキングが可能。

URL短縮サービスのビジネス価値は「分析」にあると考え、帯域幅よりもデータの価値を優先しました。

③ キャッシュ戦略:Read-Through Pattern

Read-Heavyなワークロードに耐えるため、Redisを用いた Read-Through パターンを実装しました。

  1. Redisを確認(Hitすれば即レスポンス)
  2. なければDBを確認
  3. DBの結果をRedisに書き込み(TTL付き)

https://github.com/hszk-dev/url-shortener/blob/c0161942ab09bcbd54ceccc2a7cbe7a339d12371/internal/shortener/repository.go#L64-L89

3. Testcontainersによる結合テスト

ここが今回の技術的なハイライトです。

DBを利用するテストを書く際、モック(go-sqlmock)は便利ですが、**「実際のSQLが正しく動くか」「トランザクションが効いているか」**までは保証できません。かといって、docker-compose をCI上で管理するのは面倒です。

そこで、Testcontainers for Go を採用しました。

Testcontainersとは?

GoのテストコードからDockerコンテナをプログラム的に起動・破棄できるライブラリです。
「テスト実行時のみ本物のPostgreSQLとRedisを立ち上げる」ことが可能になります。

実装例: テストヘルパー

以下のように、テスト開始時にコンテナを動的に立ち上げます。

https://github.com/hszk-dev/url-shortener/blob/c0161942ab09bcbd54ceccc2a7cbe7a339d12371/internal/shortener/integration_test.go#L23-L77

並行処理のテスト

Testcontainersを使えば、本物のDBに対して並行アクセスを行うテストも容易です。以下は、並行書き込み時にID重複が発生しないことを検証するテストです。

https://github.com/hszk-dev/url-shortener/blob/c0161942ab09bcbd54ceccc2a7cbe7a339d12371/internal/shortener/integration_test.go#L232-L260

これにより、モックでは検出できない**「レースコンディション」や「DB制約の挙動」** までテストすることができます。

まとめ

あくまで実際に運用するわけではなく全て絵空事なわけですが、実際に手を動かしてコードを書いてみると、実務に活かせる知見が多く得られました。

システム設計面接の対策として、あるいはGo言語での実践的なテストパターンの参考として、リポジトリを見ていただければ幸いです。

https://github.com/hszk-dev/url-shortener

Discussion