ADR とチーム — 意思決定プロセスの民主化
変化の兆候
ADR導入から半年。シンジは気づいていた。
変化は数字に現れていた。
| 指標 | ADR前 | ADR後6ヶ月 |
|---|---|---|
| 「なぜこうなってるか」に答えられないケース | 週に5〜10件 | 週に0〜1件 |
| 新メンバーのオンボーディング期間 | 3ヶ月 | 6週間 |
| 同じ技術比較の議論(繰り返し) | 月に3〜4回 | ほぼゼロ |
| アーキテクチャに関するPRコメント | 多い(毎回説明が必要) | 少ない(ADRに誘導) |
| インシデントでの根本原因特定時間 | 平均90分 | 平均30分 |
しかし、もっと大きな変化は数字では見えないところにあった。
チームのジュニアエンジニア、ユウキが言った。
「先週、ライブラリの選定でチームに意見を言えたんです。ADRを読んで、過去の判断の理由が分かったから。前なら黙って先輩の判断に従うだけでした」
シンジは「それだ」と思った。ADRが生んでいたのは技術的な記録だけではなかった。
ADRが生む心理的安全性
ADRは単なるドキュメントではなかった。「誰でも判断に参加できる」という文化を作っていた。
理由はシンプルだ。
過去の判断の根拠が見える → 「なんとなくこうしてきた」への挑戦が可能になる。
判断の記録がある → 「あなたのせいで失敗した」ではなく「当時の状況でベストだった、でも今は条件が変わった」と振り返れる。
プロセスが透明 → シニアだけが知っている暗黙の情報がなくなる。
ユウキが「意見を言えた」のは、彼が成長したからではない(それもあるが)。ADRが「どんな根拠でどんな判断をするか」の手本を示してくれていたからだ。
「ADR-015を読んだんです。ECS vs EKSの議論。シンジさんたちがどう比較して、どう判断したか。そのプロセスを見て、自分も同じように考えられると思った」
INFO
ADRの最大の副次効果は、技術的意思決定の「民主化」だ。シニアの暗黙知を文書化することで、誰でもアーキテクチャ議論に参加できる土台ができる。知識の非対称性がなくなることで、チームの集合知が最大化される。
RFC(Request for Comments)との違い
「ADRだけで全ての判断を記録しようとするとうまくいかない」とシンジはチームに話した。
「大きな設計変更には、ADRより前に『RFC』が必要なこともある」
ADRとRFCとDesign Docの使い分け
| ドキュメント | 目的 | タイミング | 長さ | 主な読者 |
|---|---|---|---|---|
| RFC | 設計を議論する | 判断前 | 長め | 意見を持つ全員 |
| ADR | 判断を記録する | 判断後 | 中程度 | 将来のチームメンバー |
| Design Doc | 実装を説明する | 実装前後 | 長め | 実装する人 |
「順番で考えると: RFC で議論 → ADR に記録 → Design Doc で実装詳細、だ」
RFCの書き方
RFCの例: AI推薦エンジンのマイクロサービス化の提案
# RFC-003: AI推薦エンジンをマイクロサービスとして分離する
## メタデータ
- **著者**: シンジ・タカハシ
- **状態**: 議論中
- **フィードバック期限**: 2024年9月15日
- **関連ADR**: ADR-015(ECS vs EKS)
- **Slack**: #tech-architecture
---
## 背景
現在、商品推薦ロジックはRailsのメインアプリに含まれている:
```ruby
# app/services/recommendation_service.rb(現状)
class RecommendationService
def recommend_for(user)
# 単純なルールベース推薦(協調フィルタリングなし)
user.viewed_products
.map(&:similar_products)
.flatten
.uniq
.first(10)
end
endプロダクト側から「機械学習ベースの推薦」の要求が来ている。 現在のRailsアプリにPythonのML処理を組み込むのは困難。
問題提起
- RailsアプリにPythonを組み込むと、デプロイの複雑さが増す
- ML処理のスケーリングがWebアプリのスケーリングと分離できない
- ML研究チームがRailsを触れない(Pythonしか使えない)
提案
案A: 推薦エンジンをGoで書いた独立サービスに分離
- RailsからHTTP API経由でコール
- 推薦ロジックをGoで実装(シンプルなルールベース + 将来ML)
メリット:
- 独立デプロイ・独立スケール
- MLエンジンへの将来拡張が容易
デメリット:
- サービス間通信のレイテンシ追加(約5ms)
- インフラの複雑さ増加
試作実装(Go):
// recommendation-service/main.go
package main
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type RecommendationRequest struct {
UserID int `json:"user_id"`
ViewedIDs []int `json:"viewed_product_ids"`
MaxResults int `json:"max_results"`
}
type RecommendationResponse struct {
ProductIDs []int `json:"product_ids"`
Scores []float64 `json:"scores"`
ModelVersion string `json:"model_version"`
}
func main() {
r := gin.Default()
r.POST("/recommendations", func(c *gin.Context) {
var req RecommendationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 推薦ロジック(現在はルールベース、将来はML)
recommended := getRecommendations(req.UserID, req.ViewedIDs, req.MaxResults)
c.JSON(http.StatusOK, RecommendationResponse{
ProductIDs: recommended,
Scores: calculateScores(recommended, req.UserID),
ModelVersion: "v1.0-rule-based",
})
})
r.Run(":8080")
}案B: RailsアプリにPythonサイドカーを追加
- ECSタスクにPythonコンテナを追加
- 同一タスク内でHTTP通信
メリット: サービス間ネットワーク通信がない デメリット: デプロイ単位が複雑
案C: 現状維持 + 外部MLサービス利用(AWS Personalize等)
メリット: 実装コスト低 デメリット: 月額コスト高(推定$500〜$2000/月)、カスタマイズ制限
判断に必要な情報
- ML研究チームがGoを書けるか? 書けなければ案Bかもしれない
- 推薦APIのレイテンシ要件は? 5msの追加は許容できるか?
- AWS Personalizeのコスト感: 試算したチームはいるか?
フィードバック期限
2024年9月15日
コメント
リョウ: 案Aが技術的にはクリーン。GoはシンプルなAPIサービスに向いてる。 レイテンシの5msは許容できる。
マイ: ML研究チームに確認したら「Pythonがいい」と言ってた。 Goだと採用が難しいかも。案Bか案Cを検討すべきでは?
コウスケ: AWS Personalizeを試算した。MAU 80,000で月$800程度。 初期実装コストを考えると悪くないかもしれない。
シンジ: ML研究チームの要望が重要な変数だった。 Pythonがマストなら案Bか案Cに絞る。来週MLチームと打ち合わせを入れる。
RFCで議論が終わったら、その結果をADRに書く。
```markdown
# ADR-033: AI推薦エンジンはPythonマイクロサービスとしてECSに追加する
## Status
Accepted
## Context
RFC-003でAI推薦エンジンの実装方式を議論した(2024年9月1〜15日)。
詳細はRFC-003を参照。
議論の結果、判断に決定的だった要因:
- ML研究チームがPython以外を使えない(Goは不可)
- AWS Personalizeのコスト($800/月)は現フェーズでは高い
- サイドカーパターン(案B)はデプロイ複雑性が課題
## Decision
Pythonで書いたFastAPI製のマイクロサービスをECSの別サービスとして追加する。
RailsメインアプリからはHTTP APIで呼び出す(レイテンシ: 約5ms想定)。
(以下省略)
Design Doc との連携
Design Docは「実装の詳細を説明する」ドキュメントで、ADRより長く、より実装寄りだ。
ADRが「なぜ」を語るなら、Design Docは「どうやって」を語る。
# Design Doc: AI推薦エンジンの実装設計
**ステータス**: 実装中
**ADR参照**: ADR-033 (docs/adr/033-recommendation-service.md)
**RFC参照**: RFC-003
## 概要
ADR-033で決定したPython FastAPIによる推薦サービスの実装設計。
このドキュメントはADRの「なぜ」に続く「どうやって」を説明する。
## APIインターフェース
### エンドポイント
POST /api/v1/recommendations
### リクエスト
```json
{
"user_id": 12345,
"viewed_product_ids": [100, 200, 300],
"max_results": 10,
"context": "homepage"
}レスポンス
{
"product_ids": [400, 500, 600],
"scores": [0.95, 0.87, 0.82],
"model_version": "v1.0",
"processing_time_ms": 12
}Pythonでの実装
# recommendation_service/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import numpy as np
import logging
app = FastAPI(title="Recommendation Service", version="1.0.0")
logger = logging.getLogger(__name__)
class RecommendationRequest(BaseModel):
user_id: int
viewed_product_ids: List[int]
max_results: int = 10
context: Optional[str] = "default"
class RecommendationResponse(BaseModel):
product_ids: List[int]
scores: List[float]
model_version: str
processing_time_ms: float
@app.post("/api/v1/recommendations", response_model=RecommendationResponse)
async def get_recommendations(request: RecommendationRequest):
import time
start = time.time()
try:
product_ids, scores = recommender.recommend(
user_id=request.user_id,
viewed_ids=request.viewed_product_ids,
n=request.max_results
)
elapsed = (time.time() - start) * 1000
return RecommendationResponse(
product_ids=product_ids,
scores=scores,
model_version=recommender.model_version,
processing_time_ms=round(elapsed, 2)
)
except Exception as e:
logger.error(f"Recommendation error for user {request.user_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "ok", "model_loaded": recommender is not None}Railsからの呼び出し
# app/services/recommendation_client.rb
#
# AI推薦サービスへのHTTPクライアント
# 詳細: ADR-033 docs/adr/033-recommendation-service.md
#
class RecommendationClient
BASE_URL = ENV.fetch("RECOMMENDATION_SERVICE_URL")
TIMEOUT = 0.1 # 100ms タイムアウト(SLO要件)
def self.recommend_for(user, max_results: 10)
response = connection.post("/api/v1/recommendations") do |req|
req.body = {
user_id: user.id,
viewed_product_ids: user.recently_viewed_product_ids,
max_results: max_results
}.to_json
req.headers["Content-Type"] = "application/json"
end
JSON.parse(response.body)["product_ids"]
rescue Faraday::TimeoutError
# タイムアウト時はフォールバック(ルールベース推薦)
Rails.logger.warn("Recommendation service timeout. Falling back to rule-based.")
user.popular_products(limit: max_results).map(&:id)
rescue => e
Rails.logger.error("Recommendation service error: #{e.message}")
[]
end
private
def self.connection
@connection ||= Faraday.new(url: BASE_URL) do |conn|
conn.options.timeout = TIMEOUT
conn.adapter Faraday.default_adapter
end
end
endフォールバック戦略(ADR-033のConsequencesから)
ADR-033のConsequencesに「サービス障害時のフォールバック設計が必要」と書いた。 その実装詳細がここに書いてある。
(以下省略)
「ADRとDesign Docが分かれているから、それぞれの目的が明確だ」とマイが言った。「ADRは将来も読む。Design Docは実装フェーズに読む」
## ADRを「書かされる」から「書きたい」へ
シンジがADRの導入で最も気を使ったのは「文化の醸成」だった。
「最初は『義務でやる』という雰囲気があった」とシンジはチームへのレトロスペクティブで振り返った。
### 最初の抵抗
半年前のSlack:
マイ: 「毎回ADR書くの、正直めんどくさい。コードと二重管理じゃないの?」 コウスケ: 「小さな判断まで書く必要ある? kaminariの選定とか」 リョウ: 「WIkiに書けばよくない? なんでMarkdownファイルで管理するの?」
### シンジの対応
**1. 自分が率先して書く**
シンジは自分が担当した全ての技術判断にADRを書いた。良い例示があることで、「こんな感じで書けばいい」という共通理解が生まれた。
最初の10本のうち、7本はシンジが書いた。残り3本はリョウが書いた。
**2. ADRの恩恵を「見せる」**
ADRのおかげで解決できた、という瞬間を積極的に共有した。
シンジ(Slack): 「今日、コウスケ担当のRedisキャッシュ設定を変えようとしたんだけど ADR-002を読んで『ここの設定は意図的に変えていない』と分かって 危なく壊すところだった。ADRがあってよかった」
(翌日、コウスケが自分のADRに追記を入れた)
**3. ルールではなく文化にする**
「ADRを書かないとPRがマージできない」というルールは最初は作らなかった。(後にCIで軽いチェックは入れた。)代わりに、レビューで自然にADRを活用した。
コードレビューのコメント(シンジから): 「この設定変更、なぜこの値にしたかのコンテキストをADRに残しておくと 将来の自分が助かると思います。5行でも十分です。いかがでしょう?」
「お願い」であって「強制」ではなかった。しかし毎回お願いされるうちに、自然とADRを書くようになった。
**4. ハードルを下げる**
完璧なADRを求めなかった。「5行でもいい。書いてあることが大事」
「5行ADR」の例をチームで共有した:
```markdown
# ADR-021: KaminariをページネーションGemとして使う
## Status
Accepted
## Context
一覧ページのページネーション実装が必要。will_paginateとKaminariが候補。
## Decision
Kaminariを採用。チームの複数プロジェクトで使用実績があり、Rails 7との
互換性問題がwill_paginateで報告されているため。
## Consequences
チームの学習コストがゼロ。将来100万件超のページネーションが必要になった場合は
cursor-based paginationへの移行を検討する。
「これで十分。3ヶ月後の誰かが『なぜKaminari?』と思ったとき、ゼロよりはるかにましな情報がある」
チームのADRレビュー文化
ADRが増えてきたとき、シンジはコードレビューと同じようにADRレビューの文化を作った。
ADRレビューで見るべき視点
コードレビューと違い、ADRレビューで見るべき点は「コードの正しさ」ではない。
ADRレビューのチェックポイント:
1. 「コンテキストが理解できるか?」
→ このPRを見ていない人が読んでも、なぜこの判断が必要だったか分かるか?
→ 数字や具体例が含まれているか?
2. 「トレードオフが正直に書かれているか?」
→ 良いことしか書いていない場合、何を省いているか聞く
→ 「デメリットや注意事項がゼロ」は疑ってかかる
3. 「代替案が書かれているか?」
→ なぜA案を選んで、B案を選ばなかったか?
4. 「将来への言及があるか?」
→ 何があればこの判断を見直すか?
5. 「6ヶ月後の自分が読んで分かるか?」
→ 今の文脈から切り離して読んでみる
実際のADRレビューコメント(GitHub PR上):
# docs/adr/031-postgresql-connection-pooling.md
## Context セクションへのコメント
リョウ: 「GoodJobを使っていた背景(ADR-003)への参照があると、
なぜ今Sidekiqに変えるのかの対比が分かりやすくなると思います。
"以前はGoodJobを使っていた(ADR-003参照)。以下の理由でSidekiqに移行する"
という書き方はどうでしょう?」
---
## Consequences セクションへのコメント
マイ: 「Sidekiqはスレッドモデルなので、グローバル変数・クラス変数に
スレッドセーフでないコードがある場合に問題が出ます。
このリスクへの言及があると良いと思います。
実際、以前の別プロジェクトでこれにハマったことがあります」
---
## Decision セクションへのコメント
コウスケ: 「選ばなかった理由にGoodJob継続が入っていないのは意図的ですか?
『GoodJobで継続する』という選択肢の評価も書いてあると、
将来のGoodJoBへの回帰判断の際に役立つと思います」これらのコメントを受けて、ADRの品質が上がった。「ADRのレビューがコードのレビューと同じくらい真剣になってきた」とリョウが言った。
INFO
ADRレビューの質を上げるには、レビュアーが「将来の読者として」ADRを読むことが重要だ。「このPRの文脈を知っている人」として読むと、コンテキストが不足していても気づかない。意図的に「PRを知らない人」の視点で読む。
外部への説明力
ADR導入のもう一つの予期しない恩恵が生まれた。
技術的な意思決定を外部(投資家、採用候補、顧客)に説明しやすくなったのだ。
採用面接でのやり取り:
候補者(シニアエンジニア): 「なぜRuby on Railsを使っているんですか?
今ならGoやTypeScriptを選ぶ会社が多いですが」
シンジ: 「ADR-001に詳細な選定理由を書いています。
公開可能な部分でいうと、2022年当時のチーム構成・採用市場・
優先要件のトレードオフを評価した結果です。
入社後にそのドキュメントを読んでもらえれば背景が分かります。
もし今同じ選定をするなら、違う判断をする部分もあります。
それについても話せますよ」
候補者: 「ADRで意思決定を記録しているチームは珍しいですね。
入社後も議論に参加できそうで魅力を感じました」
採用後のオンボーディングも変わった:
# 新メンバーオンボーディングガイド(抜粋)
## Week 1: 読むべきドキュメント
### Day 1〜2: ADRを読む
`docs/adr/README.md` から始めて、以下の順番で読んでください:
1. ADR-001(なぜRailsか)
2. ADR-008(なぜPostgreSQLか)
3. ADR-012(認証方式)
4. ADR-015(インフラ設計の方針)
これを読むことで、このシステムがなぜこのアーキテクチャになっているかが
80%分かります。残り20%はコードを読みながら埋まっていきます。
### Day 3: コードを読む
ADRを読んだ後にコードを読むと、設計の意図が分かります。
分からないことはADRを見てください。それでも分からなければ聞いてください。ナオ(8ヶ月前に入社したシニアエンジニア)が言った。
「ADRを読んでからコードを読む、という順番が本当に効果的だった。普通は逆で、コードを読んでから『なぜこの設計?』と疑問が出る。ADRがあると、最初から『なぜ』を理解した状態でコードを読める」
ADR文化を守る「難しい判断」
ADR文化が定着してくると、別の問題が出てきた。
「ADRを書くのが面倒くさいから、ADR不要と判断する」という行動が起きた。
マイが言った。「コウスケがPRを出してきたんだけど、ADRに書くほどではないと判断して省略してる。でも私から見たら書くべき判断に見える」
シンジは話し合いの場を設けた。
「コウスケ、そのPRのlib/billing_service.rbの変更、なぜADR不要と判断した?」
「課金ロジックの改善で、設計の変更はしてない。バグ修正に近い」
「将来誰かが同じ変更をしようとしたとき、このPRのコンテキストがないと判断できないと思わないか?」
コウスケは考えた。「……確かに、課金ロジックの変更は将来影響が出やすいな。書いたほうがいいか」
判断の基準を改めて共有した:
# ADRが必要かの判断基準(チームガイドライン)
**以下のいずれかに当てはまる場合はADRを書く:**
- 6ヶ月後の自分が「なぜこうなっているか」と疑問を持ちそう
- 他のエンジニアが変更する可能性があり、文脈がないと間違えそう
- 複数の選択肢を比較して選んだ
- 「触るな」とコメントしたいコードを書いた
- セキュリティ・パフォーマンスに影響する判断
- 外部依存(gem, AWS, 外部API)の選定
**ADR不要の判断:**
- バグ修正で設計の意図は変わっていない
- 命名・フォーマット等のコードスタイル変更
- テストケースの追加
- 変更の理由がコミットメッセージで十分に説明できるWARNING
ADR文化が定着すると「ADR不要」という判断も増える。これ自体は健全だ。ただし「ADRを書くのが面倒だから不要と判断する」という本末転倒に注意。判断基準を共有して、チームで一貫性を保つ。
シニアとジュニアの関係の変化
ADR導入から半年後のスプリントレビューで、ユウキが発表した。
「今回のバッチ処理の実装で、ADR-003とADR-031を読んで、Sidekiqのジョブ設計を自分で判断できました。べき等性の確保についてはADR-031のConsequencesにガイドラインが書いてあって、それを参照して実装しました。シンジさんに確認しなくても判断できました」
マイが言った。「半年前なら絶対シンジさんに確認してたやつだ」
シンジはうなずいた。ADRは「先輩が答えを持っている」という構造を変えていた。
「答えはドキュメントにある。それを読んで、自分で判断できる」という構造に。
これは単なる効率化ではない。チームの自律性と成長への投資だ。シニアが答えを一人で持ち続ければ、ジュニアは育たない。ADRはシニアの判断プロセスを「見える化」することで、ジュニアの成長を促す。
「ADRって、教育コストを下げるんじゃなくて、教育の質を上げるんだな」とユウキが言った。
「正確にはそうだ」とシンジ。「ADRは答えじゃなくて、答えの出し方を教える」
最終章では、ADR文化が定着した後の世界と、シンジのチームがどう変わったかを振り返る。そして、あなたのチームでADRを始めるための具体的なスタートラインを示す。