エピローグ — アーキテクチャは進化する
1年後
βリリースから1年が経った。カオリのECプラットフォームは、目標の10万ユーザーを大きく超え、35万ユーザーに成長していた。チームも6人から18人になった。
「カオリさん、そろそろ在庫管理をマイクロサービスに分けませんか?デプロイのたびに全機能が止まるのが辛いです」
テックリードになったユウが言った。1年前に「マイクロサービスにしよう」と言っていた彼が、今では「適切なタイミング」を理解して議論している。
「ようやくその時が来たね。でもどこから始めるか、慎重に考えよう」
INFO
最良のアーキテクチャは、最初から完璧なものを設計することではなく、現在のニーズに応えながら、将来の変化に対応できる余地を残すことです。そして、適切なタイミングで進化させることです。
移行の原則
1. 境界は実績から学ぶ
# 1年間で明確になったモジュール境界
# engines/inventory/ → 独立したデプロイが必要(高頻度変更・高負荷)
# engines/ordering/ → 安定(変更が少ない)
# engines/catalog/ → 中程度(週1-2回変更)
# engines/users/ → 安定(変更が少ない)
# engines/analytics/ → 独立が望ましい(重いバッチ処理)
# 1年動かしてみてわかったこと:
# - 在庫管理はブラックフライデーで異常負荷
# - 注文処理はほぼ一定の負荷
# - カタログは検索最適化のためRedis依存が強い「最初から予想できていたことはない。1年間のデータが、正しい分割点を教えてくれた」
2. ストラングラーパターンで段階移行
# フェーズ2: モジュール境界を厳格化(現在)
# config/initializers/module_boundaries.rb
if Rails.env.development?
# 直接的な境界越えを検出するルール
ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
# Inventoryモジュールのテーブルに、Ordering以外からアクセスしていたら警告
if event.payload[:sql].include?('inventory_') &&
!caller.any? { |c| c.include?('engines/inventory') || c.include?('engines/ordering') }
Rails.logger.warn "モジュール境界違反: inventory への不正アクセス\n#{caller.first(5).join("\n")}"
end
end
end
# フェーズ3への準備: 在庫モジュールのAPIを整備
module Inventory
class Client
# 将来的にHTTP APIに差し替えられるように
def self.check_availability(product_id:, quantity:)
# 今はEngine内のコードを呼ぶ
Inventory::AvailabilityChecker.new.call(
product_id: product_id,
quantity: quantity
)
end
def self.reserve(product_id:, quantity:, order_id:)
Inventory::Reserver.new.call(
product_id: product_id,
quantity: quantity,
order_id: order_id
)
end
end
end3. サービス抽出の具体的手順
在庫サービス抽出の計画(6週間):
Week 1-2: 準備
- Inventory::Client の全呼び出し箇所を確認
- インターフェースを確定(API仕様書を書く)
- データベースの分離方針を決める
Week 3-4: 新サービス開発
- Rails API として inventory-service を作成
- Inventory エンジンのコードを移植
- テストを新サービスに移行
Week 5: シャドウモード
- モノリスと新サービスに両方リクエストを送る
- 結果を比較して一致を確認
- 本番ではモノリス側の結果を使用
Week 6: カットオーバー
- Inventory::Client をHTTP APIに差し替え
- モノリスの在庫テーブルを廃止
- モニタリングを強化技術的負債の管理
# 技術的負債のトラッキング
# 「意図的な負債」と「偶発的な負債」を区別する
# 意図的な負債の例:ドキュメントに記録する
# ADR-015: OrdersControllerのバリデーション重複を許容する
# 理由: 今週のリリースに間に合わせるため
# 返済計画: 次のスプリントで共通バリデーターに抽出する
# 見積もりコスト: 半日
# 偶発的な負債の例:発見次第修正する
# - テストがないコードパス
# - コピペされたビジネスロジック
# - 不明確な変数名負債の可視化
# lib/tasks/tech_debt.rake
namespace :tech_debt do
desc '技術的負債のレポートを生成'
task report: :environment do
puts "=== 技術的負債レポート ==="
# テストカバレッジ
coverage = `bundle exec rspec --format progress 2>&1 | tail -1`
puts "テストカバレッジ: #{coverage}"
# 循環複雑度の高いメソッド
puts "\n複雑なメソッド(flogスコア上位10):"
`bundle exec flog --all app/**/*.rb | head -20`.lines.each do |line|
puts " #{line}"
end
# 長すぎるクラス
puts "\n長すぎるクラス(200行以上):"
Dir['app/**/*.rb'].each do |file|
lines = File.readlines(file).length
puts " #{file}: #{lines}行" if lines > 200
end
end
endアーキテクチャの継続的改善
フィットネスファンクション
アーキテクチャの品質を自動的にチェックする「フィットネスファンクション」。
# spec/architecture/module_boundaries_spec.rb
RSpec.describe 'モジュール境界の遵守' do
it 'Inventoryモジュールに直接アクセスしていない(Orderingを除く)' do
# ordering以外のコードがinventoryのクラスに直接アクセスしていないことを確認
inventory_references = Dir['app/**/*.rb', 'engines/**/*.rb'].filter_map do |file|
next if file.include?('engines/inventory/')
next if file.include?('engines/ordering/') # Orderingは直接アクセス許可
next if file.include?('spec/')
content = File.read(file)
file if content.match?(/Inventory::[A-Z]/)
end
expect(inventory_references).to be_empty,
"以下のファイルがInventoryモジュールに不正アクセス:\n#{inventory_references.join("\n")}"
end
it 'Controller層にビジネスロジックがない' do
thick_controllers = Dir['app/controllers/**/*.rb'].filter_map do |file|
content = File.read(file)
# ActiveRecord の複雑なクエリがコントローラにある
file if content.scan(/\.where\(|\.joins\(|\.includes\(/).length > 3
end
expect(thick_controllers).to be_empty,
"Fatなコントローラを検出:\n#{thick_controllers.join("\n")}"
end
endArchitecture Decision Review
「ADRは書いたら終わりじゃない」とカオリはチームに言った。
# ADR-001 振り返り(6ヶ月後)
## 結果
モジュラーモノリスの選択は正しかった。
## 良かったこと
- 3ヶ月でのβリリースを達成できた
- チーム全員がコードベースを把握できている
- 単一デプロイによりデプロイ頻度が週3-4回を実現
## 課題
- Inventoryモジュールがブラックフライデーで負荷問題
- 18名になり、デプロイの衝突が月2-3回発生
- 管理画面のクエリが遅くなってきた(CQRS導入検討)
## 更新
見直しのトリガー「月間ユーザー35万・チーム18名」に達した。
在庫管理のサービス分割プロジェクトを開始する(ADR-016参照)。AWSインフラの進化
# 1年間のインフラ進化
初期(3ヶ月前):
- ALB → ECS(Rails) → RDS → ElastiCache
- Lambda: メール・バッチのみ
- コスト: 月約8万円
現在(本番):
- ALB → ECS(Rails, 3タスク) → RDS Multi-AZ → ElastiCache Cluster
- EventBridge + 5つのLambda
- CloudFront(静的アセット)
- コスト: 月約35万円
計画(6ヶ月後):
- 在庫サービスを独立(ECS別クラスター)
- 読み取りレプリカ(ダッシュボード用)
- SQS + Lambda(在庫更新の非同期化)
- コスト見込み: 月約55万円(スケールに見合うと判断)次のアーキテクトへ
カオリは1年間の学びをノートにまとめた。
ソフトウェアアーキテクチャを1年間やってみて学んだこと:
1. アーキテクチャは「今の問題」を解くもの
明日の問題のために複雑な設計を導入しても、
今日のチームが理解できなければ意味がない。
2. シンプルさは最も難しい
シンプルな設計を保つには、不要な複雑さを
「追加しない」という継続的な意思決定が必要。
3. 記録することが最重要
なぜその設計にしたかを書いていないと、
6ヶ月後には誰も思い出せない。
ADRは設計そのものより価値があることがある。
4. チームが理解できる設計だけが生き残る
10人のチームで1人しか理解できない設計は、
実質的に機能しない設計だ。
5. 移行は常に可能だ
「最初からマイクロサービスにしておくべきだった」
という後悔は無意味。今から正しく分割すればいい。
良い設計は、移行を可能にする境界を持っている。
6. 技術に惚れるな、問題に惚れろ
クリーンアーキテクチャが好きだから適用するのではなく、
テスタビリティの問題を解決するために選ぶ。
ツールは目的のための手段だ。
INFO
あなたが今直面しているアーキテクチャの問題は、この本で学んだパターンのどれかで解決できるかもしれません。ただし、常に「なぜこのパターンが生まれたのか」「どんな問題を解決するのか」を問い続けてください。パターンの名前を知ることではなく、問題を解決することが目的です。
旅の終わりと始まり
この1年、カオリは多くのアーキテクチャパターンを学び、実装し、失敗し、改善した。
モノリスから始まり、レイヤード、クリーン、ヘキサゴナル、CQRS、イベントソーシング、SOA、サーバーレス、イベント駆動—それぞれに存在する理由があり、輝く文脈があった。
しかし、最終的に学んだ最も重要なことは、パターンの名前ではなかった。
それは「なぜ」を問い続けることだった。
「なぜこの設計が問題を解決するのか?」「なぜ今の文脈でこれが最適なのか?」「なぜこの境界はここにあるのか?」
この問いを持ち続けるエンジニアだけが、本当の意味でのアーキテクトになれる。
カオリは、次のジュニアエンジニアに同じ質問を投げかけた。「アーキテクチャ設計書、真っ白なドキュメントを前にして、何から始める?」
それがあなたへの問いでもある。
この本を読んでくれたあなたへ
全13章の旅、お疲れ様でした。カオリが学んだように、アーキテクチャは「決める」ものではなく「育てる」ものです。
コードを書くたびに、設計を見直すたびに、この本に戻ってきてください。同じ章でも、あなたの経験が増えるたびに、違う視点から読めるようになるはずです。
良いコードを。良い設計を。そして、良い問いを。