mybook

エピローグ — システム設計の思考法

ハルトの旅の終わり(と始まり)

2年が経った。

EchoTaskは月間アクティブユーザー100万人を突破した。1台のVPSから始まったシステムは、複数のAWSリージョンをまたぐマイクロサービス群へと変貌を遂げた。

Loading diagram...

ハルトは午後の会議室でコーヒーを飲んでいた。隣に座ったのは、入社3ヶ月の新人エンジニア、ミサキだ。

「先輩……来月、転職面接を受けようと思っているんです。システム設計の面接が怖くて。」

ミサキは小さな声で打ち明けた。ハルトはカップを置いて、静かに微笑んだ。

「それは——EchoTaskで僕が学んだことと同じだよ。一緒にやってみよう。」


設計面接ロールプレイ

「Twitterライクなシステムを設計してください」

ハルトが面接官役を引き受けた。


ハルト(面接官): 「Twitterのようなシステムを設計してください。時間は45分です。」

ミサキ(応答者): 「まず要件を確認させてください。ツイートの投稿・閲覧・フォロー機能が核となる機能でしょうか?」

ハルト: 「そうです。タイムライン、フォロー/フォロワー関係も含みます。」

ミサキ: 「ユーザー規模はどれくらいを想定しますか?」

ハルト: 「月間アクティブユーザー1億人で考えてください。」

ミサキ: 「分かりました。では規模を推定します。DAU(日次アクティブ)を1割と仮定すると1000万人。1人あたり1日20ツイート読む、3ツイート書くとすると……」

# ミサキの皮算用
monthly_users = 100_000_000
dau = monthly_users * 0.1      # 1000万人
 
tweet_reads_per_day  = dau * 20  # 2億リクエスト
tweet_writes_per_day = dau * 3   # 3000万リクエスト
 
read_qps  = tweet_reads_per_day / 86400.0   # 約2315 QPS
write_qps = tweet_writes_per_day / 86400.0  # 約347 QPS
 
# ピーク(平均の3倍)
peak_read_qps  = (read_qps * 3).ceil   # 約6946 QPS
peak_write_qps = (write_qps * 3).ceil  # 約1042 QPS
 
puts "Read:  avg #{read_qps.round} QPS / peak #{peak_read_qps} QPS"
puts "Write: avg #{write_qps.round} QPS / peak #{peak_write_qps} QPS"

ミサキ: 「読み取りがピークで約7000 QPS、書き込みは約1000 QPSです。読み取り優位なシステムですね。」

ハルト: 「よいですね。では概略設計を描いてみてください。」

ミサキ: 「まずシンプルに3層で考えます。クライアント → ロードバランサー → アプリサーバー → DB。タイムラインは読み取りが多いのでキャッシュを分離します。」

ハルト: 「データベーススキーマはどう設計しますか?」

ミサキ: 「まず主要な3テーブルです。」

# Railsのmigrationで表現
create_table :users do |t|
  t.string  :handle,       null: false  # @username
  t.string  :display_name, null: false
  t.integer :followers_count, default: 0
  t.integer :following_count, default: 0
  t.timestamps
end
 
create_table :tweets do |t|
  t.references :user,    null: false, foreign_key: true
  t.text       :content, null: false  # 280文字以内
  t.integer    :likes_count,   default: 0
  t.integer    :retweet_count, default: 0
  t.timestamps
end
 
create_table :follows do |t|
  t.references :follower,  null: false, foreign_key: { to_table: :users }
  t.references :followee,  null: false, foreign_key: { to_table: :users }
  t.timestamps
  t.index [:follower_id, :followee_id], unique: true
end

ハルト: 「タイムラインはどう実装しますか?フォロワー1000万人のセレブユーザーのツイートをどう配信しますか?」

ミサキ: 「……難しいですね。2つのアプローチがあると思います。Pull型はユーザーがタイムラインを開くたびにフォロイーのツイートをDBから取得する。でも1000万フォロワーが同時にログインしたら?」

ハルト: 「1000万クエリが同時に発行されますね。」

ミサキ: 「なのでPush型も併用します。ツイート投稿時にフォロワー全員のタイムラインキャッシュ(RedisのList)に書き込む。ただしセレブユーザーはフォロワーが多すぎるので、セレブのツイートはPullのまま残してマージします。ハイブリッド方式です。」

ハルト: 「いいですね。それはFanout on Write + Fanout on Read のハイブリッドです。APIはどう設計しますか?」

ミサキ: 「RESTで3つのエンドポイントを基本に。」

POST   /api/v1/tweets              # ツイート投稿
GET    /api/v1/timelines/home      # ホームタイムライン取得
POST   /api/v1/users/:id/follows   # フォロー
DELETE /api/v1/users/:id/follows   # フォロー解除

ハルト: 「最後に、このシステムのボトルネックと改善案を話してください。」

ミサキ: 「タイムラインの書き込み増幅です。フォロワー100万人のユーザーが1ツイートすると100万件のキャッシュ書き込みが発生します。非同期キューで処理し、優先度を下げてバックグラウンドで実行します。あとはホットスポット——人気ツイートの読み取り集中は、CDNとインメモリキャッシュの多段構成で対処します。」

ハルト: 「——合格ですよ、ミサキさん。」


INFO

面接のコツ

  • 最初の5分は必ず要件確認。「何人のユーザー?読み書きの比率は?」を聞く
  • シンプルな設計から始め、面接官の誘導に乗って深掘りする
  • デメリットを自分から言うことで「トレードオフを理解している」と示せる
  • 計算は桁が合えばよい。細かい正確さより思考プロセスを見せる

全章の振り返り

テーマ学んだことEchoTaskでの適用例
1章プロローグ1台から始めることの価値VPS単体でMVPをリリース
2章スケーリング垂直→水平スケールの判断基準ECSタスク数のAuto Scaling設定
3章ロードバランシングALBとヘルスチェックの仕組みBlue/Greenデプロイの安全な切り替え
4章DB設計インデックス・クエリ最適化タスク一覧のN+1問題を解消
5章DBスケーリングAuroraリードレプリカ・シャーディング読み取りを3台のレプリカに分散
6章キャッシュRead-Through / Write-Through / TTLElastiCacheでAPIレイテンシを80%削減
7章CDNエッジキャッシュとオリジン保護CloudFrontで静的アセットを全世界配信
8章非同期処理Sidekiq + SQSのキュー設計メール通知をバックグラウンドに分離
9章マイクロサービス分割の判断基準とサービスメッシュ通知・課金・検索サービスを独立化
10章API GatewayJWT認証・レートリミット・ルーティング全サービスへの統一エントリーポイント
11章データ一貫性Sagaパターン・OutboxパターンとAT LEAST ONCEタスク作成と通知の分散トランザクション
12章可用性Multi-AZ・Circuit Breaker・カオスエンジニアリングAZ障害でもゼロダウンタイムを実現
13章オブザーバビリティログ・メトリクス・トレーシングの三本柱X-Rayでマイクロサービスのボトルネック特定
14章セキュリティIAM最小権限・WAF・暗号化SQLインジェクション・SSRF攻撃をWAFで遮断

エンジニアとしてのキャリアパス

「ミサキ、システム設計を学んだ先に何があると思う?」

ハルトはホワイトボードに図を描いた。

Loading diagram...

「バックエンドエンジニアとしてシステム設計を学ぶと、インフラの判断ができるようになる。インフラを学ぶとSREの仕事が見えてくる。この本で学んだことは、そのすべての土台だよ。」

バックエンドエンジニア(1〜3年目)

  • APIの設計・実装、DBスキーマ設計
  • キャッシュ戦略、クエリ最適化
  • 非同期処理・キューの実装

インフラエンジニア(3〜5年目)

  • AWS/GCPのアーキテクチャ設計
  • CI/CDパイプライン構築、Terraform によるIaC
  • コスト最適化・セキュリティ強化

SRE / Platform Engineer(5年目〜)

  • SLO/SLA設計、エラーバジェット管理
  • カオスエンジニアリング・ゲームデー運営
  • 開発チームの生産性向上(内部プラットフォーム設計)

Golang移行の判断基準

「先輩、うちのサービスってずっとRailsで行くんですか?」

「良い質問だ。」ハルトはRailsとGolangを比較した表を書いた。

判断基準RailsのままGolangへ移行
チームのGo経験Goを知らない複数人がGoを書ける
開発速度機能開発が最優先パフォーマンスが最優先
スループット要件ピーク500 QPS以下ピーク5000 QPS以上
メモリ使用量許容範囲内OOMでECSタスクが落ちる
コンパイル型の恩恵不要型安全・バイナリ配布が必要
移行コスト高い(書き直し)ペイする(長期的に)
// Goへ移行した場合の通知サービス例
// Railsのsidekiqワーカーをそのまま置き換えられる
 
package main
 
import (
    "context"
    "encoding/json"
    "log"
    "os"
 
    "github.com/aws/aws-sdk-go-v2/service/sqs"
)
 
type NotifyJob struct {
    UserID  int64  `json:"user_id"`
    Message string `json:"message"`
    Channel string `json:"channel"` // "email" | "push"
}
 
func processMessage(ctx context.Context, body string) error {
    var job NotifyJob
    if err := json.Unmarshal([]byte(body), &job); err != nil {
        return err
    }
    // チャネル別に通知ロジックを分岐
    switch job.Channel {
    case "email":
        return sendEmail(ctx, job)
    case "push":
        return sendPush(ctx, job)
    }
    return nil
}
 
func main() {
    // SQSからメッセージをポーリング
    // Railsの Sidekiq と同じ役割だがメモリは1/10
    log.Println("notification worker started")
    _ = os.Getenv("SQS_QUEUE_URL")
    // ... SQSポーリングループ
}

「EchoTaskでは通知サービスだけGoに移行した。メモリ使用量が240MBから28MBに下がって、ECSタスク数を半分にできた。」

WARNING

Goへの移行は慎重に Railsの生産性(CoC、ActiveRecord、豊富なgem)を捨てるコストは大きい。まず「ボトルネックを計測」し、Goへの書き直しがペイする部分だけを移行するストラングラーパターンを使うこと。全体を一気に書き直すのはリスクが高い。


推薦リソース

「本も教えてもらえますか?」とミサキが聞いた。

システム設計

  • 「Designing Data-Intensive Applications」(Martin Kleppmann) — データシステムの理論的基盤。分散システムを深く理解したいなら必読
  • 「System Design Interview」(Alex Xu) — 設計面接の定番。具体的な事例が豊富
  • AWS Well-Architected Framework — 無料。AWS上の設計原則を網羅

データベース

  • 「PostgreSQL: Up and Running」 — RDBの実践書。インデックス設計・チューニングに強くなれる
  • 「NoSQL Distilled」(Martin Fowler) — NoSQLの使い分けを理論から理解する

セキュリティ

  • OWASP Top 10 — Webアプリの代表的脆弱性。毎年読み返す価値がある
  • 「The Web Application Hacker's Handbook」 — 攻撃者視点でセキュリティを学ぶ

ブログ・エンジニアリングブログ

  • High Scalability — 実際の大規模システムのアーキテクチャ事例
  • AWS Architecture Blog — AWSを使った設計パターンの宝庫
  • Netflix Tech Blog / Slack Engineering — 先進的なシステム設計の事例

次に学ぶべきトピック

ハルトはリストをメモアプリに打ち込んで見せた。

[ ] Kubernetes (k8s)
    - ECSの次のステップ。コンテナオーケストレーションの標準
    - EKSでRailsアプリをデプロイし、HPA(水平Pod自動スケール)を体験する

[ ] Apache Kafka
    - SQSより強力なイベントストリーミング基盤
    - イベントソーシング・CDC(Change Data Capture)のユースケース

[ ] Terraform
    - AWSリソースをコードで管理(IaC)
    - 「コンソールをポチポチする」から卒業できる

[ ] Service Mesh(Istio / AWS App Mesh)
    - マイクロサービス間の通信を透過的に管理
    - mTLS・サーキットブレーカーをアプリコードなしに実現

[ ] eBPF / Cilium
    - Linuxカーネルレベルのネットワーク・セキュリティ観測
    - 次世代のオブザーバビリティ基盤として注目

[ ] OpenTelemetry
    - ベンダーロックインしないトレーシング・メトリクス標準
    - Datadog / New Relic / Jaegerへのエクスポートが統一APIで可能

「どれから始めればいいですか?」

「転職後の業務で使うもの、でいいよ。学習は「今必要なもの」から始めるのが一番続く。」

学習ロードマップの目安

Year 1: バックエンドの基礎を固める
  → Rails / Go で API を作り、PostgreSQL をちゃんと使う
  → Docker でローカル環境を整え、ECRへイメージをpush できる状態に

Year 2: インフラとCI/CDを自分で触る
  → Terraform でVPC・RDS・ECSを自分で構築する
  → GitHub Actions でテスト→ビルド→デプロイのパイプラインを引く

Year 3: 可観測性とセキュリティに投資する
  → OpenTelemetryでトレーシングを導入、SLO/エラーバジェットを定義する
  → AWSのセキュリティベストプラクティス(SCPs、PrivateLink、GuardDuty)を体得する

Year 4+: スケールと設計の複雑さに挑む
  → Kubernetes / Service Mesh / Kafka に手を出す
  → チームをリードするアーキテクト・SREのロールに挑戦する

EchoTaskの未来 — グローバル展開

「EchoTaskは海外展開しないんですか?」

「実は来年、北米と欧州に展開する計画がある。」

グローバル展開に向けて、ハルトはマルチリージョン構成を設計していた。

東京リージョン(ap-northeast-1)  ← アジア・日本ユーザー
  └ Aurora Global Cluster(Primary)
  └ ElastiCache
  └ ECS Cluster

バージニア北部(us-east-1)  ← 北米ユーザー
  └ Aurora Global Cluster(Secondary / Read)
  └ ElastiCache
  └ ECS Cluster

フランクフルト(eu-central-1)  ← 欧州ユーザー(GDPR対応)
  └ Aurora Global Cluster(Secondary / Read)
  └ ElastiCache
  └ ECS Cluster

Route 53 → Latency-Based Routing → 最寄りリージョンへ誘導
CloudFront → グローバルエッジ(130+箇所)

GDPRへの対応 欧州ユーザーのデータをEUリージョン内に留めるデータレジデンシーが必須になる。Aurora Global Clusterでは書き込みは東京Primary経由になってしまうため、欧州ユーザーのWrite操作はフランクフルトのPrimary Clusterを別途立て、アプリ層でユーザーの地域に応じてルーティングする設計が必要だ。

「グローバル展開って、コードより法律と運用が大変なんだよ。」ハルトは苦笑した。


トレードオフの哲学

「結局、設計で一番大事なことは何ですか?」

ミサキの問いに、ハルトはしばらく黙った。

コンテキストを読むこと、だよ。」

「え?」

「同じ問題でも、スタートアップと大企業では答えが違う。1万人のユーザーと1億人では違う。チームが3人と100人でも違う。

完璧なシステムはない。あるのはそのコンテキストでのベストなトレードオフだけ。

面接でも、「この会社が今何を最優先にしているか」を読んで答えるのが本当に上手い人のやり方だよ。」

# 良いシステム設計の兆候
GOOD_DESIGN_SIGNALS = [
  "新人が2時間で全体を理解できる",
  "障害が起きたとき、どこで何が起きているか分かる",
  "機能追加が1つのサービスだけに閉じている",
  "スケールが必要な部分だけスケールできる",
  "朝3時にPagerDutyが鳴らない",
].freeze
 
# 悪いシステム設計の兆候
BAD_DESIGN_SIGNALS = [
  "誰もシステム全体を把握していない",
  "変更が怖くて手が出せない",
  "新機能に3つのサービスの変更が必要",
  "全体をスケールしないと動かない",
  "ドキュメントが常に古い",
].freeze

INFO

「計測してから最適化する」— Rob Pike 推測するな、計測せよ。最適化の順序は常に「1. 計測(どこが遅いか?)→ 2. ボトルネック特定 → 3. 最も効果的な改善を1つ → 4. 再計測」。体感でチューニングしたシステムは、たいてい間違った場所を最適化している。


ミサキへの言葉

会議室の窓から夕日が差し込んでいた。

「先輩は、エンジニアになって後悔したことはありますか?」

ミサキが不意に聞いた。

ハルトは少し考えて、首を横に振った。

「EchoTaskが最初に落ちた夜、ユーザーから怒りのメールが100件来た。そのときは死にたかったよ。」

「……」

「でも、その夜に直した。翌日、ユーザーから『すぐ直してくれてありがとう』ってメールが来た。そのとき思った——システムは生き物だって。作って終わりじゃない。育てるものだって。」

ハルトは続けた。

「設計面接の答えを暗記しようとしないこと。大切なのは、なぜそれを選ぶのかを自分の言葉で説明できること。

EchoTaskを作りながら、僕は何度も失敗した。1台のサーバーがダウンして、スロークエリで苦しんで、通知が重複して、セキュリティインシデントが起きて。

でも、その失敗から学んだことが、今の判断基準になっている。

本で読んだ知識より、手を動かして失敗した経験の方が、ずっと深く刻まれる。

自分でプロダクトを作り、スケールの問題にぶつかり、解決策を試してみること——それが最高の勉強法だ。」


EchoTaskの現在

月間アクティブユーザー100万人。エンジニア30人のチーム。最初の1台のVPSから始まったシステムは、今や:

  • ECSタスク: 80台以上(Auto Scaling)
  • Auroraクラスター: 3サービスそれぞれに独立
  • ElastiCacheクラスター: 2つ
  • CloudFrontディストリビューション: 2つ
  • SQSキュー: 12本
  • マイクロサービス: 6つ

しかし、ハルトが最も誇りに思うのはスペックではない。

「朝3時にPagerDutyが鳴らなくなったこと」

それが、システム設計を学んだ意味だ。


「ありがとうございました、先輩。面接、頑張ってみます。」

ミサキは立ち上がってお辞儀をした。

「うちの面接を受けてみる気はない?」

ミサキは顔を上げた。ハルトはニヤリと笑っていた。

「……え?」

「EchoTaskに必要なのは、こういう質問をちゃんとする人間だよ。スペックじゃなくて、考える姿勢がある人。」

窓の外、夕日がオレンジに染まっていた。

EchoTaskの物語は、まだ続いている。


INFO

この章のキーポイント

  • 設計面接は「要件確認 → 規模推定 → 概略設計 → データスキーマ → API設計 → ボトルネック議論」の順で進める
  • 「なぜそれを選ぶのか」をトレードオフとともに自分の言葉で説明できることが最重要
  • シンプルな設計から始め、必要になったときに複雑にする(YAGNI)
  • 計測してから最適化する。推測でチューニングしない
  • コンテキストを読む——スタートアップと大企業では正解が違う
  • 失敗から学ぶことが最高の教科書になる