ADR の書き方 — テンプレートと実例
最初のADRに挑戦
シンジはチームに宿題を出した。
「今週中に、自分が担当している機能で一番重要な技術判断を一つ選んで、ADRに書いてきてほしい」
翌週、最初に出てきたのはリョウのADRだった。
# ADR-002: RedisをキャッシュレイヤーとしてAmazon ElastiCacheで運用する
## Status
Accepted
## Context
ユーザー一覧APIのレスポンスタイムが800ms超えていた。
キャッシュが必要。
## Decision
Redisを使う。
## Consequences
速くなった。シンジは読んで、静かに言った。
「……これだと、なぜRedisなのか、なぜElastiCacheなのか、何も分からない。6ヶ月後の誰かにとって、このADRは意味をなさない」
「でも判断したのはRedisだって分かるじゃないですか」
「判断だけは分かる。でも私たちが知りたいのは判断じゃなくて、その判断に至った理由とトレードオフなんだ」
リョウは少しむっとした顔をした。「じゃあ何を書けばいいんですか。全部書いてたら時間がかかりすぎる」
「全部じゃなくていい。でも『なぜ?』に答える情報は必要だ。一緒に直してみよう」
良いADRの条件
シンジはホワイトボードに「良いADRの5条件」を書いた。
1. 文脈が明確: なぜこの判断が必要になったのか、どんな制約があったのか
コンテキストがないと、「なぜこの判断をしたか」の意味が分からない。「なぜRedisか」よりも「なぜキャッシュが必要だったか」から書く。
2. 候補が列挙されている: 他に何を検討したか、なぜそれを選ばなかったか
「なぜAを選んだか」と「なぜBを選ばなかったか」はセットで意味を持つ。片方だけでは不完全だ。
3. 理由が具体的: 「良いと思ったから」ではなく「〇〇という理由で」
「Redisのほうが良い」ではなく「Redisはデータ構造が豊富で、将来のランキング機能にSortedSetが使えるため」と書く。
4. デメリットも正直に: 良いことだけでなく、捨てたもの・リスクも書く
完璧に見えるADRは信頼されない。「〇〇というデメリットがあるが、そのトレードオフを受け入れた」と書くことで実用的な記録になる。
5. 将来への言及: この判断をいつ見直すべきか
「〇〇になったら見直す」という再評価のトリガーを書く。ADRを「生きているドキュメント」にする仕組みだ。
リョウのADRを書き直す
シンジはリョウと一緒に、ADR-002を書き直した。
# ADR-002: キャッシュレイヤーにAmazon ElastiCache (Redis)を採用する
## Status
Accepted
## Context
2023年10月、ユーザー一覧APIのP95レスポンスタイムが850msになり、
SLO(500ms以下)を恒常的に違反するようになった。
ボトルネック調査の結果:
- ユーザー一覧クエリがDB全件スキャンになっていた
- 1時間あたりのリクエスト数: 約50,000
- ユーザーデータの更新頻度: 1日数十回(読み取り:書き込み = 1000:1)
- 対象ユーザー数: 約120,000件
候補として検討したソリューション:
1. Amazon ElastiCache (Redis)
2. Amazon ElastiCache (Memcached)
3. EC2上の自前Redis
4. DBインデックスの最適化のみ
5. RDSリードレプリカの追加
## Decision
Amazon ElastiCache (Redis) を採用する。
RailsアプリとはRubyのredis gemで接続し、
`Rails.cache.fetch` を使ったフラグメントキャッシュを実装する。
Redisを選んだ理由:
- データ構造の豊富さ: Memcachedにはないhash/list/setが使える
(将来のランキング機能などに有用)
- 永続化オプション: AOFによるデータ保持がMemcachedより優れる
- チームのRedis使用経験: Memcachedより馴染みがある
- Railsとの親和性: cache_store設定が`:redis_cache_store`で即対応
ElastiCacheを選んだ理由(自前EC2上のRedisと比較):
- 運用負荷: パッチ適用・フェイルオーバーがAWS管理になる
- コスト: t3.microインスタンスで月額約$15(EC2管理工数を考慮すると安い)
- マルチAZ: ElastiCacheはMulti-AZ構成が容易
DBインデックス最適化のみを選ばなかった理由:
- クエリの複雑さから、インデックスだけでは250ms以下達成が困難と判断
- 将来的なデータ増加でも対応できるキャッシュ層が必要
RDSリードレプリカを選ばなかった理由:
- コスト増大(リードレプリカは月額$100以上追加)
- 読み取りが1000:1の比率で圧倒的に多いが、リードレプリカで解決できるのは
DB層のみ。キャッシュのほうが効果が大きいと判断
## Consequences
良い影響:
- キャッシュヒット時のレスポンスタイム: 5ms以下(目標の500msを大幅達成)
- DBサーバーへのクエリ数が1/50に削減
- スケールアウトが容易: ElastiCache Clusterの追加が簡単
悪い影響・リスク:
- キャッシュの整合性管理が必要になる
(ユーザー更新時に `Rails.cache.delete` を忘れるとバグになる)
- インフラコストが月額約$15増加
- Redisがダウンしたときのフォールバック設計が必要
(現状: Redisダウン時はキャッシュなしで直接DBを叩く設計)
実装での注意事項:
- キャッシュキーは `user_list_v1_#{digest}` 形式でバージョン管理
- TTLはデフォルト15分。ユーザー更新時に明示的に削除
- キャッシュキーに `_v1_` を付ける理由: 将来のスキーマ変更時にバージョンを上げれば
古いキャッシュが無効化される
再評価のトリガー:
- ユーザー更新頻度が大幅に増えてキャッシュ効率が落ちた場合
- ElastiCacheのコストがDB改善コストを超えた場合
- Redisのダウンタイムが月間SLOに影響するほど増えた場合リョウは読んで、少し沈黙した。
「……確かに、こっちのほうが半年後の俺が助かる」
「この違いを覚えてほしい。最初のADRは『判断だけ書いた』。書き直したADRは『判断に至る思考プロセスを書いた』。後者が未来に価値を持つ」
テンプレートの詳細解説
各セクションで何を書くべきか、指針を整理しよう。
Contextセクション: 「なぜこの判断が必要だったか」
Contextは過去形・現在形で書く。「当時の状況」を説明するセクションだ。
書くべき内容:
- どんな問題が起きていたか(具体的な数字があると良い)
- どんな制約があったか(チーム規模、コスト、スキルセット)
- どんな選択肢を検討したか
- どんな要件・優先順位があったか
## Context
### 問題の背景
新規ユーザー登録フローで、メール送信が同期処理になっており、
APIのレスポンスタイムが平均3秒を超えていた。
具体的な指標:
- 登録フローのP95レスポンスタイム: 3,200ms
- ユーザーからのクレーム: 週に3〜4件(「登録が遅い」)
- モバイル端末でのタイムアウトエラー発生率: 2%
### 技術的制約
- 既存インフラ: AWS ECS (Fargate) + RDS (PostgreSQL)
- チーム規模: バックエンド3名
- 月間コスト予算の余裕: 約$100
- 今後半年でモバイルアプリのリリース予定
### 検討した選択肢
1. Sidekiq + Redis
2. Amazon SQS + Lambda
3. GoodJob (PostgreSQL-backed)
4. 同期処理のまま(タイムアウト延長)Context で「具体的な数字」を書くことが重要だ。「遅かった」ではなく「3,200msだった」。「問題があった」ではなく「週に3〜4件のクレームがあった」。
これにより、将来の再評価が「感覚」ではなく「数字」で行える。
Decisionセクション: 「何を、なぜ選んだか」
Decisionは現在形・能動態で書く。「〜にする」「〜を採用する」という形。
書くべき内容:
- 何を選んだか(明確に1文で)
- なぜ選んだか(選択した理由)
- 選ばなかった選択肢の理由(これが特に重要)
## Decision
GoodJob をバックグラウンドジョブライブラリとして採用する。
### 採用理由
GoodJobはPostgreSQLをキューストレージとして使用するため、
新たなインフラ(RedisやSQS)が不要。
- **インフラシンプル化**: 既存のRDS PostgreSQLを流用。追加インフラコストゼロ
- **Rails統合**: ActiveJobのアダプターとして動作。既存コードの変更が最小
- **可視性**: PostgreSQLのJobテーブルをSQLで直接確認できる。デバッグが容易
- **チーム経験**: 過去プロジェクトでの使用実績あり
### 選ばなかった理由
**Sidekiq を選ばなかった理由:**
Sidekiqは高パフォーマンスだが、Redisが別途必要。
現時点でのジョブ量(1日約1,000件)ではGoodJobで十分。
$15/月のElastiCacheコストと運用負荷を避けたい。
ただし、ジョブ量が1日10万件を超えたら移行を検討する。
**SQS + Lambda を選ばなかった理由:**
Lambdaのコールドスタートとデプロイの複雑さが、チームの現状スキルでは負担。
SaaS規模ではなくスタートアップ段階での過剰設計と判断。
また、Lambdaの可観測性(ログ・エラー追跡)の複雑さがGoodJobより高い。
**同期処理継続を選ばなかった理由:**
3秒のレスポンスタイムはモバイルユーザーの離脱に直結。
UXへの影響が定量的に確認されており(タイムアウトエラー2%)、根本解決が必要。「選ばなかった理由」を書くことで、将来「なぜSidekiqじゃないの?」という疑問に先んじて答えられる。これが議論の繰り返しを防ぐ。
Consequencesセクション: 「何が変わるか」
Consequencesは将来形・事実で書く。良い面と悪い面を正直に書く。
WARNING
「良いことだけ書いたADR」は信頼されない。デメリットやリスクを正直に書くことで、将来の変更判断に使える実用的な記録になる。「問題点を隠したADR」は、問題が起きたときに誰にも参照されなくなる。
## Consequences
### ポジティブな影響
- メール送信が非同期化され、APIレスポンスが200ms以下になる
- Rails標準のActiveJobインターフェースを使うので、将来Sidekiqへの移行も容易
- PostgreSQLのJobテーブルを直接確認でき、キューの可視性が高い
- 追加インフラなし。既存RDSで動作
### ネガティブな影響・リスク
- DBへの書き込み負荷が微増する
(ただし、現在のDBスペックでは問題ない水準と試算。IOPS余裕80%)
- GoodJobのプロセスをECSで常時起動させる必要がある
(FargateタスクとしてWebとWorkerを分ける構成が必要。月額約$20追加)
- PostgreSQLのコネクション数に注意が必要
(Worker数を増やすとDB接続数が増える。現在の最大接続数500の制限内に収める)
### 技術的負債・将来への考慮
- ジョブ数が1日10万件を超えたら、Sidekiqへの移行を検討する
- GoodJobのConcurrencyはWebサーバーの2倍程度に設定すること
- ジョブのべき等性(重複実行への対応)は実装者の責任。ADR-023でガイドラインを作成予定よくある悪いADRのパターン
シンジはチームに「避けるべきパターン」をまとめた。
パターン1: 理由なき決定
# 悪い例
## Decision
MySQLではなくPostgreSQLを使う。
PostgreSQLのほうが良いと思うから。# 良い例
## Decision
PostgreSQLを採用する。
理由:
1. JSONBカラムの使用を予定しており、PostgreSQLのJSONB型は
MySQLのJSONより高速なインデックスをサポート(参考ベンチマーク: 3x差)
2. PostGISへの将来的な拡張(位置情報機能の追加要件がある)
3. チーム全員のPostgreSQL運用経験がMySQLより豊富(全5名が1年以上の使用経験)
4. Amazon RDS for PostgreSQLは当社の他システムでも採用済みで
運用ノウハウが蓄積されているパターン2: デメリットを書かない
# 悪い例
## Consequences
- パフォーマンスが向上する
- スケーラビリティが上がる
- 開発速度が上がる# 良い例
## Consequences
良い影響:
- P95レスポンスタイムが3,000ms → 150msに改善(ベンチマーク測定値)
- 非同期処理によりAPIの返答が速くなり、ユーザー体験が向上
悪い影響:
- 非同期処理になることでデバッグが複雑になる(ログを追いにくい)
- べき等性の確保が必要(ジョブの重複実行対策が必要。実装コスト2〜3日程度)
- 障害時にジョブが失われるリスク(retryとdead_letter_queueの設計が必要)
- ローカル開発環境でWorkerプロセスを別途起動する必要がある(開発体験の若干の悪化)パターン3: コンテキストが薄すぎる
# 悪い例
## Context
キャッシュが必要だったのでRedisを入れた。# 良い例
## Context
2024年Q1、DAUが10万を超えたことでDBの読み取りクエリが
ボトルネックになった。具体的には:
- 商品検索APIのP99レスポンスタイム: 2,400ms(目標値: 300ms以下)
- 毎分のDBクエリ数: 12,000(DBのmax_connectionsの90%に達していた)
- 読み取り:書き込みの比率 = 95:5
- RDSのCPU使用率: 平均85%(CloudWatchアラート多発)
この時点でのインフラ構成:
- DB: Amazon RDS PostgreSQL (db.r6g.large, リードレプリカなし)
- アプリサーバー: ECS Fargate (Webタスク x5)
- 月次成長率: DAU約15%増
調査結果:
- クエリのうち70%は商品リスト取得
- 商品データの更新頻度は1日5〜10回(ほぼ静的)
- キャッシュで読み取りの大半を削減できると試算パターン4: 選択肢を書かない
# 悪い例
## Decision
AWS Cognitoを認証サービスとして採用する。
理由: マネージドサービスで運用が楽だから。# 良い例
## Decision
AWS Cognitoを認証サービスとして採用する。
検討した選択肢と選ばなかった理由:
**Auth0 を選ばなかった理由:**
機能的に優れているが、MAU 100,000での月額コストが約$3,500。
Cognitoの同規模コストは約$730。年間約$33,000の差は無視できない。
また Auth0 のカスタムドメインメール送信がBusinessプラン以上必要で追加コスト。
**自前実装(Devise)を選ばなかった理由:**
セキュリティ(パスワードハッシュ、総当たり攻撃対策)の実装・メンテを自前で持つのは
チームの現状スキルと工数的にリスクが高い。
MFA対応も将来要件として挙がっており、Deviseでの実装コストが高い。
**Cognito を選んだ理由:**
- AWSインフラとの統合が容易(IAM, ALB, API Gateway との連携)
- MFA/ソーシャルログインが標準機能
- 無料枠: 月間50,000 MAUまで無料
- チームのAWS経験が豊富(移行コストが低い)ADRの長さ
シンジがよく聞かれるのが「どのくらい書けばいいか」という質問だ。
目安を示した。
| 判断の重要度 | ADRの長さ目安 | 例 |
|---|---|---|
| 小(特定ライブラリの選定) | 10〜20行 | kaminariかwill_paginateか |
| 中(コンポーネント設計) | 30〜60行 | 非同期処理の方式 |
| 大(アーキテクチャ変更) | 60〜150行 | DB移行、認証方式変更 |
| 特大(組織的影響) | 150行以上 | マイクロサービス化 |
INFO
「完璧なADR」より「書いたADR」のほうが価値がある。最初は短くていい。チームで議論するうちに、必要な情報が増えていく。「書けなかった」よりも「短すぎた」のほうがはるかにマシだ。
Railsコードとの連携パターン
ADRはコードに参照リンクを書くことで、コードリーディング中に自然と参照される。
# app/jobs/send_welcome_email_job.rb
#
# バックグラウンドジョブの方式選定については ADR-007 を参照:
# docs/adr/007-background-job-library.md
#
class SendWelcomeEmailJob < ApplicationJob
queue_as :default
# ジョブのリトライ設定はADR-007のConsquences節を参照
retry_on Net::SMTPError, wait: :polynomially_longer, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end# config/initializers/cache_store.rb
#
# ElastiCache (Redis) をキャッシュストアとして使用
# 選定理由: ADR-002 参照 → docs/adr/002-elasticache-redis.md
#
# キャッシュキーの命名規則:
# {model}:{scope}:v{version}:{params_digest}
# バージョン番号を含めることでスキーマ変更時の強制無効化が可能(ADR-002参照)
Rails.application.configure do
config.cache_store = :redis_cache_store, {
url: ENV.fetch("REDIS_URL"),
expires_in: 15.minutes,
race_condition_ttl: 10.seconds,
error_handler: ->(method:, returning:, exception:) {
Rails.logger.error("Redis cache error: #{exception.message}")
}
}
end# app/models/product.rb
class Product < ApplicationRecord
# キャッシュの実装については ADR-002 に記載のポリシーに従う
after_save :invalidate_cache
after_destroy :invalidate_cache
def self.cached_active_list
Rails.cache.fetch("products:active:v2", expires_in: 30.minutes) do
active.includes(:category).order(:name).to_a
end
end
private
def invalidate_cache
Rails.cache.delete("products:active:v2")
end
endADRのレビューチェックリスト
シンジはコードレビューと同様に、ADRレビューのチェックリストを作った。
# ADRレビューチェックリスト
## Contextセクション
- [ ] 問題・背景が具体的な数字と共に書かれているか?
- [ ] 技術的制約(チーム規模、コスト、スキル)が明記されているか?
- [ ] 検討した選択肢がすべて列挙されているか?
## Decisionセクション
- [ ] 何を決めたかが1文で明確に書かれているか?
- [ ] 選んだ理由が「〇〇だから」と具体的に書かれているか?
- [ ] 選ばなかった理由が書かれているか?
## Consequencesセクション
- [ ] 良い影響だけでなく、悪い影響・リスクも書かれているか?
- [ ] 将来の再評価トリガーが書かれているか?
- [ ] 実装での注意事項が書かれているか?
## 全体
- [ ] 「6ヶ月後の自分」が読んで理解できるか?
- [ ] 技術を知らない人でも文脈が掴めるか?
- [ ] このPRのコードと整合性が取れているか?ユウキの初めてのADR
翌週、ユウキが初めてADRを書いてきた。
# ADR-004: CSVインポート機能にRubyのCSVライブラリを使用する
## Status
Accepted
## Context
管理画面から商品データを一括インポートするCSV読み込み機能が必要。
ファイルサイズ: 最大5MB、最大10,000行のCSVファイル。
検討した選択肢:
1. Ruby標準ライブラリ `csv`
2. `smarter_csv` gem
3. `roo` gem(Excel含む複数形式対応)
## Decision
Ruby標準ライブラリの `csv` を使用する。
選んだ理由:
- 追加gemが不要(依存関係が増えない)
- 10,000行程度のCSVなら標準ライブラリで十分なパフォーマンス
- チーム全員が馴染みのあるAPI
選ばなかった理由:
`smarter_csv`: ヘッダー行の自動処理などの機能は便利だが、
今回のCSVフォーマットは固定されているため、オーバースペック。
依存gemを増やすリスクを取る価値がない。
`roo`: Excelファイルのインポートは現時点では要件なし。
将来Excelが必要になったら移行を検討する。
## Consequences
良い影響:
- 追加依存なし
- Ruby標準ライブラリのため、APIの変化リスクが低い
悪い影響:
- 大きなCSVファイル(100,000行以上)を将来処理する場合は
streaming処理への書き換えが必要
(現時点では要件外のため問題なし)
再評価のトリガー:
- インポートファイルサイズが50MB以上になった場合
- Excel(.xlsx)インポートの要件が出た場合シンジは読んで言った。
「よく書けてる。選ばなかった理由も書いてあって、将来の再評価トリガーもある。これが最初のADRとは思えない」
ユウキは少し照れながら言った。「ADR-002の書き直し版を手本にして書きました」
これが文化の伝播だ。良いADRが手本になり、次の良いADRを生む。
INFO
ADRは書けば書くほど上手くなる。最初の1本は不完全でいい。「書いた」という事実が、次の人の手本になる。そして5本書いたチームは、50本書いたときにはプロになっている。
次の章では、ADRの実例として「データベース選定」を取り上げ、PostgreSQL vs MySQLの判断プロセスをシンジのチームの実際の議論を通じて学ぶ。