エピローグ — パターンを組み合わせる力
12週間後
「アヤカさん、できました」
ユウキが自信に満ちた顔でPRのリンクを送った。プロローグで見た800行のファットコントローラー、orders_controller.rb のリファクタリングだ。
アヤカがレビューを開いた。コードを見るより前に、PRの説明文が目に入った。
PR #247: orders_controller.rb のリファクタリング
変更内容:
- CQRS: 読み取りをQueryオブジェクトに分離
- レイヤードアーキテクチャ: OrderCreationServiceへのビジネスロジック移行
- リポジトリパターン: OrderRepository, ProductRepository の導入
- イベント駆動: 注文確定後の処理をEventBus経由に変更
- バックプレッシャー: RateLimiterとOrderQueueServiceの導入
- サーキットブレーカー: 外部API呼び出しに適用
Before: 800行のモノリシックコントローラー
After: 35行のコントローラー + 各責任をもつクラス群
テスト: 47件追加(全てパス)
「いいね」アヤカが静かに言った。
コードを見ると:
# Before: 800行のファットコントローラー
class OrdersController < ApplicationController
def create
# バリデーション、在庫チェック、合計計算、クーポン、保存、
# メール、ポイント、Slack...全部ここに(800行)
end
end
# After: 各パターンが適切な場所に
class OrdersController < ApplicationController
before_action :authenticate_user!
before_action :check_rate_limit, only: [:create]
# Query Side(CQRSパターン)
def index
@order_summaries = OrderListQuery.new.call(
user_id: current_user.id,
page: params[:page]&.to_i || 1,
status: params[:status]
)
end
def show
@order = OrderDetailQuery.new.call(
order_id: params[:id].to_i,
user_id: current_user.id
)
rescue OrderNotFoundError
redirect_to orders_path, alert: "注文が見つかりません"
end
# Command Side(CQRSパターン + バックプレッシャー + サーキットブレーカー)
def create
result = OrderQueueService.new.enqueue(
user_id: current_user.id,
items: order_params[:items],
priority: current_user.vip? ? :vip : :normal
)
render json: {
message: "注文を受け付けました",
job_id: result[:job_id],
status_url: order_status_path(result[:job_id])
}, status: :accepted
rescue QueueFullError
render json: { error: "只今注文が集中しています。しばらくお待ちください。" },
status: :service_unavailable
rescue RateLimiter::ExceededError
render json: { error: "注文は1分に5件まで" },
status: :too_many_requests
end
def status
result = OrderQueueService.new.status(params[:job_id])
render json: result
end
private
def check_rate_limit
limiter = RateLimiter.new(key: "orders:#{current_user.id}", limit: 5, window: 60)
raise RateLimiter::ExceededError unless limiter.allowed?
end
def order_params
params.require(:order).permit(:coupon_code, items: [:product_id, :quantity])
end
endパターンの配置図
ユウキのリファクタリングには、12週間で学んだパターンが全て入っていた。
各層の責任:
| 層 | パターン | 役割 | コードの場所 |
|---|---|---|---|
| ネットワーク | バックプレッシャー | トラフィック制御 | API Gateway / Rack::Attack |
| コントローラー | MVC / CQRS | リクエスト振り分け | controllers/ |
| アプリケーション | レイヤードアーキテクチャ | ユースケース | services/ |
| データ | リポジトリパターン | DB抽象化 | repositories/ |
| 通信 | サーキットブレーカー | 外部障害遮断 | clients/ |
| 非同期 | イベント駆動 | 疎結合な後処理 | events/, handlers/ |
| 整合性 | サーガパターン | 分散トランザクション | sagas/ |
AWS全体アーキテクチャ
パターン選択の判断フレームワーク
12週間でユウキが身につけた「どのパターンを選ぶか」の判断基準:
問題の種類で選ぶ
「コードの整理がしたい、責任が混在している」
→ MVCパターン + レイヤードアーキテクチャ
「同じクエリが色々な場所に散らばっている」
→ リポジトリパターン + クエリオブジェクト
「コントローラーが300行を超えている」
→ サービスオブジェクト(Application Layer)
「注文一覧ページが遅い」
→ CQRSパターン → 読み取りレプリカ → キャッシュ
「処理が増えるたびにコードが増える、密結合」
→ イベント駆動アーキテクチャ
「データ変換ロジックが複雑で追いにくい」
→ パイプ&フィルタパターン
「外部APIが遅くてユーザーを待たせる」
→ 非同期化(ActiveJob)+ サーキットブレーカー
「マイクロサービス間で整合性が取れない」
→ サーガパターン(オーケストレーション or コレオグラフィ)
「レガシーを直したい(止めずに)」
→ ストラングラーフィグパターン
「高負荷でシステムが崩壊する、セール対策」
→ バックプレッシャー + レート制限 + キューイング
アンチパターン:よくある間違い
12週間で学んだ「やってはいけないこと」も重要だ。
# アンチパターン1: パターンの過剰適用
# 3行で済む処理にリポジトリ・サービス・コマンドを作る
class GetUserNameService
def initialize(user_repository)
@user_repository = user_repository
end
def call(user_id)
@user_repository.find(user_id).name # User.find(id).name でいい
end
end
# アンチパターン2: レイヤーを飛び越える
class OrdersController
def create
# コントローラーがDBに直接書く
Order.create!(params) # NG: サービス層を飛ばしている
Product.decrement!(:stock) # NG: ビジネスロジックがコントローラーに
end
end
# アンチパターン3: 間違ったパターン選択
# 小さなアプリにサーガパターン
class UserRegistrationSaga # 1サービスの処理にサーガは不要
# マイクロサービス間でないなら単なるトランザクションでよい
end
# アンチパターン4: パターン名の乱用
class UserService # 何でもServiceと名付ける
def create_user; end
def send_email; end
def calculate_discount; end
def generate_report; end
# 何をするクラスかわからない
end
# 正しい命名(責任が明確)
class UserCreationService; end # ユースケース
class EmailNotificationService; end # インフラ
class DiscountCalculator; end # ドメイン
class SalesReportQuery; end # クエリWARNING
パターンは「問題が先、パターンが後」。問題がないのにパターンを適用すると、複雑さだけが増える。「これを使えば良いコードになる」ではなく、「この問題にはこのパターンが適切」という順番で考える。
進化的アーキテクチャ
「ユウキ、一番大事なことを教える」
アヤカが珍しく真剣な顔をした。
「アーキテクチャは一度作ったら終わりじゃない。システムが成長するにつれて、アーキテクチャも進化させる必要がある」
フェーズ1(初期: ~1万ユーザー)
シンプルなMVC + モノリス
単一のRDS、シンプルなスキーマ
Heroku / Railway でも十分
↓ ユーザーが増えてクエリが遅くなる
フェーズ2(成長期: ~10万ユーザー)
レイヤードアーキテクチャ + CQRS
RDS Read Replica + ElastiCache
ECS / Fargate への移行
↓ 機能が増えてコードが複雑になる
フェーズ3(拡大期: ~100万ユーザー)
イベント駆動 + サーガ
SNS/SQS でサービス間通信
サーキットブレーカー導入
↓ トラフィックが急増する
フェーズ4(成熟期: ~1000万ユーザー)
バックプレッシャー + レート制限
CDN + WAF + API Gateway
マルチAZ、DR設計
↓ 特定機能を独立させたい
フェーズ5(分散期)
ストラングラーフィグで段階的分離
サービスメッシュ(Istio等)
分散トレーシング(OpenTelemetry)
「最初から全部やる必要はない?」
「YAGNI(You Ain't Gonna Need It)。今必要でないものは作らない。でも、パターンを知っていれば、必要になったときに素早く導入できる。知っているが使わないと知らないは全然違う」
コードベースの健康指標
「パターンを適切に使えているか、どうやって確認しますか?」ユウキが聞いた。
「コードメトリクスで確認できる」
# lib/tasks/code_health.rake
namespace :code_health do
desc "コントローラーの行数チェック"
task controller_size: :environment do
fat_controllers = []
Dir.glob("app/controllers/**/*.rb").each do |path|
lines = File.readlines(path).count
if lines > 100
fat_controllers << { path: path, lines: lines }
end
end
if fat_controllers.any?
puts "警告: ファットコントローラーが検出されました"
fat_controllers.each { |c| puts " #{c[:path]}: #{c[:lines]}行" }
else
puts "OK: 全コントローラーが100行以下です"
end
end
desc "循環参照チェック"
task circular_deps: :environment do
# 上位レイヤーが下位レイヤーを参照していないか確認
violations = []
Dir.glob("app/models/**/*.rb").each do |path|
content = File.read(path)
if content.match?(/require.*controllers|include.*Controller/)
violations << "#{path}: モデルがコントローラーを参照"
end
end
violations.empty? ? puts("OK: 循環参照なし") : violations.each { |v| puts "警告: #{v}" }
end
end学習の次のステップ
# これからユウキが学ぶこと
next_steps = {
architecture: [
"ヘキサゴナルアーキテクチャ(ポートとアダプター)",
"クリーンアーキテクチャ",
"ドメイン駆動設計(DDD)" # 本サイトに別トピックあり
],
aws: [
"EKS(Kubernetes on AWS)",
"AWS CDK でインフラをコードで管理",
"OpenTelemetry による分散トレーシング",
"AWS AppMesh / Service Mesh"
],
rails: [
"Hotwire / Turbo でリアルタイム更新",
"Action Cable WebSocket",
"Rails Engine でのモジュール分割",
"Zeitwerk オートローディング"
],
golang: [
"Go でのマイクロサービス実装",
"gRPC によるサービス間通信",
"Go の並行処理パターン"
]
}INFO
アーキテクチャパターンは手段であり、目的はユーザーへの価値提供。「良いコード」を書くことではなく「良いプロダクト」を作ることが最終目標。パターンはそのための道具。道具の使い方を学んだ今、次は「いつ、どの道具を使うか」の判断力を磨く番。
ユウキの12週間
「PRのコメントを読んで」
アヤカがPRにコメントを入れていた。
LGTM
12週間前のあなたなら、これを全部コントローラーに書いていた。
今のあなたは、問題を見て「これはCQRSで解ける」「ここにサーキットブレーカーが必要」
「この非同期処理はイベント駆動にすべき」と言える。
それがアーキテクチャを学んだということ。
コードを書く力は大事。でも、設計を議論できる力はもっと大事。
チームで同じ言葉で話せるようになったこと——それが今日の一番の成果。
次は DDD(ドメイン駆動設計)に挑戦してみて。
ユビキタス言語、境界付きコンテキスト、集約……
今あなたが持っている基礎の上に、さらに大きな概念が積み重なる。
ユウキはしばらくコメントを読んでいた。
12週間前、800行のコードを見てただ「読みにくい」と思っていた。今は違う。どこに何があるべきかが見える。変更がどこに影響するかが見える。新しい機能をどこに追加すべきかが見える。
そして何より、設計の話がチームメンバーと5分でできるようになった。
「MVCで言うとここがController相当で……」「これはCQRSを適用すべき場所だと思います」「サーキットブレーカーがないとカスケード障害が起きます」——そういった言葉が自然に出るようになっていた。
それが、パターンを学んだということだった。
まとめ:12のパターン
| パターン | 解決する問題 | キーワード | 学んだ章 |
|---|---|---|---|
| MVCパターン | 責任の混在 | モデル・ビュー・コントローラー | 2章 |
| レイヤードアーキテクチャ | ロジックの散在 | 関心の分離 | 3章 |
| リポジトリパターン | クエリの散在 | データアクセスの抽象化 | 4章 |
| イベント駆動 | 密結合な後処理 | 疎結合・非同期 | 5章 |
| パイプ&フィルタ | データ変換の複雑さ | フィルターチェーン | 6章 |
| CQRSパターン | 読み取りのパフォーマンス | 読み書きの分離 | 7章 |
| サーガパターン | 分散トランザクション | 補償トランザクション | 8章 |
| ストラングラーフィグ | レガシーの刷新 | 段階的移行 | 9章 |
| サーキットブレーカー | カスケード障害 | 遮断・フォールバック | 10章 |
| バックプレッシャー | 過負荷崩壊 | レート制限・キューイング | 11章 |
次のプロジェクトへの持ち帰りチェックリスト
「次の案件でどう使うか、具体的なチェックリストを作っておくといい」とアヤカが言った。
# 新しいプロジェクト参画時のチェックリスト(Rakeタスク形式)
namespace :architecture do
desc "プロジェクト健康診断"
task :health_check do
puts "=== アーキテクチャ健康診断 ===\n\n"
checks = {
"MVCの責任分離" => -> { check_fat_controllers },
"レイヤー構造の存在" => -> { check_service_layer },
"リポジトリパターン" => -> { check_repository_usage },
"イベントの疎結合" => -> { check_event_driven },
"CQRSの適用箇所" => -> { check_cqrs_pattern },
"障害対応の仕組み" => -> { check_circuit_breaker },
}
checks.each do |name, check|
result = check.call
status = result[:ok] ? "✓" : "!"
puts " #{status} #{name}: #{result[:message]}"
end
end
private
def check_fat_controllers
fat = Dir.glob("app/controllers/**/*.rb").select { |f| File.readlines(f).count > 100 }
fat.empty? ? { ok: true, message: "全コントローラー100行以下" }
: { ok: false, message: "ファット: #{fat.map { |f| File.basename(f) }.join(', ')}" }
end
def check_service_layer
has_services = Dir.exist?("app/services") && Dir.glob("app/services/**/*.rb").any?
has_services ? { ok: true, message: "app/services/ あり" }
: { ok: false, message: "サービス層なし — レイヤードアーキテクチャを検討" }
end
def check_repository_usage
has_repos = Dir.glob("app/repositories/**/*.rb").any?
has_repos ? { ok: true, message: "リポジトリパターン採用済み" }
: { ok: false, message: "リポジトリなし — クエリがモデルに散在していないか確認" }
end
def check_event_driven
has_events = Dir.glob("app/events/**/*.rb").any? || defined?(ActiveSupport::Notifications)
has_events ? { ok: true, message: "イベント機構あり" }
: { ok: false, message: "イベント駆動なし — 後処理が密結合になっていないか確認" }
end
def check_cqrs_pattern
has_queries = Dir.glob("app/queries/**/*.rb").any?
has_queries ? { ok: true, message: "Queryオブジェクトあり" }
: { ok: false, message: "Queryオブジェクトなし — 重い読み取りクエリがサービスに埋まっていないか確認" }
end
def check_circuit_breaker
has_cb = Gem.loaded_specs.key?("circuitbox") || Gem.loaded_specs.key?("resilient")
has_cb ? { ok: true, message: "サーキットブレーカーライブラリあり" }
: { ok: false, message: "障害対応なし — 外部API呼び出しがある場合は要検討" }
end
endINFO
このチェックリストをプロジェクト参画初日に実行すると、技術的な会話のたたき台ができる。「リポジトリがない」「サービス層がない」という発見は改善提案の起点になる。
パターンを使った設計議論の例
パターンを学ぶと、設計の議論が変わる。ユウキが実際に経験した変化を振り返った。
学ぶ前:
ユウキ: 「この処理、なんかコードが長くて読みにくいですね…」
上司: 「直せる?」
ユウキ: 「えっと、ちょっと難しいかも…」
学んだ後:
ユウキ: 「OrdersControllerが800行あります。バリデーション、DB保存、
メール送信、ポイント計算が混在しています。
まずApplication Serviceを切り出してコントローラーを
薄くしましょう。次のスプリントで着手可能です。」
上司: 「工数は?」
ユウキ: 「4時間で完了できます。リグレッションリスクは低く、
既存のRSpecがカバーしています。」
語彙があると、会話が具体的になる。具体的になると、行動できる。
Golang版の設計視点
Railsで学んだパターンは言語を超える。Goプロジェクトでも同じパターンが使われる。
// パターンの対応関係
// Rails: app/services/order_creation_service.rb
// Go: internal/usecase/order_usecase.go
package usecase
type OrderUseCase struct {
repo repository.OrderRepository // リポジトリパターン
publisher event.Publisher // イベント駆動
cb circuitbreaker.CircuitBreaker // サーキットブレーカー
}
func (u *OrderUseCase) CreateOrder(cmd CreateOrderCommand) (*Order, error) {
// Application Service と同じ責任
if err := cmd.Validate(); err != nil {
return nil, err
}
order, err := u.repo.Save(NewOrder(cmd))
if err != nil {
return nil, err
}
// イベント発行(疎結合)
u.publisher.Publish(OrderCreatedEvent{OrderID: order.ID})
return order, nil
}「RailsとGoは文法が違う。でも OrderUseCase を見た瞬間に、Railsの OrderCreationService と同じ構造だとわかる。パターンの名前を知っているから」
INFO
パターンを学ぶことは、特定の言語やフレームワークへの依存から脱することでもある。設計の考え方は普遍的で、RailsエンジニアがGoへ転向するとき、Djangoエンジニアがに移行するときも、パターンの知識は新しい言語の設計を読む地図になる。
ユウキの次のステップ
「12章を終えたユウキへ。次に学ぶとよいことを残しておく」アヤカが Notion にページを作った。
■ DDDへの入り口
- ユビキタス言語: チームとコードで同じ用語を使う
- 集約: 整合性の境界を定義する
- 境界付きコンテキスト: 大きなシステムを分割する
■ マイクロサービス設計
- サービス分割の基準(境界付きコンテキストが基礎)
- サービス間通信(同期/非同期の使い分け)
- 分散トレーシング(Datadog APM, OpenTelemetry)
■ 可観測性
- ログ設計(構造化ログ + コンテキスト)
- メトリクス設計(何を計測するか)
- SLO/SLAの設計と運用
■ チームのための設計
- ADR(Architecture Decision Records)の書き方
- 技術的負債の管理と可視化
- コードレビューでの設計フィードバックの与え方
「全部を一度にやる必要はない。今のプロジェクトで1つ問題を見つけ、1つパターンを適用する。それを繰り返す」
ユウキはノートを閉じた。
12週間で変わったのは、コードの書き方だけではなかった。問題の見方が変わった。「なぜこうなっているか」「どうすればよくなるか」を考える習慣がついた。
それが、エンジニアとして成長するということだった。
「パターンは知識ではなく、筋肉だ。使うことで身につく。」 — アヤカ
「設計の議論ができるようになったとき、エンジニアは一段階成長する。」 — ユウキ(12週間後)