ADR とは何か — 意思決定を記録する文化
月曜朝のプレゼン
シンジはホワイトボードの前に立ち、チーム全員を見渡した。
「ADR、つまりArchitecture Decision Recordsを導入したい」
マイが即座に反応した。「また新しいドキュメント?今でもWikiが死んでるのに」
「今回は違う」とシンジは言った。「ADRはWikiじゃない。コードと同じリポジトリに入れる、バージョン管理された意思決定の記録だ」
リョウが眉を上げた。「コードと一緒に?」
「そう。コードを変えるとき、なぜその設計にしたかをMarkdownファイルに書いてPRに含める。それだけ」
「それって、コードレビューと一緒にドキュメントレビューもするってこと?」とコウスケ。
「そう。でも難しくない。Gitのコミットメッセージと同じ感覚で書けばいい。長くなくていい。5行でも価値がある」
チームの空気が少し変わった。全員がシンジの話を聞いていた。
ADRの起源: Nygardの提唱
ADRという概念は、2011年にMichael Nygardが提唱した。彼のブログ記事「Documenting Architecture Decisions」が起源だ。
当時Nygardは、大企業の大規模システムで技術リードをしていた。そこで直面したのはシンジと同じ問題だった。
「なぜこのシステムがこのように設計されているかを理解するために、私は何ヶ月もかけてコードを読み、古いメールを漁り、もはや会社にいない人たちに連絡を取った。そのコストは膨大だった。」
Nygardが提唱したADRは、このシンプルな洞察から生まれた。
「コードはバージョン管理されている。しかし、なぜそのコードになったかはどこにある?」
プロダクトが長く続くほど、設計判断の「なぜ」が失われていく。新しいメンバーが入っても、既存の実装を信頼していいか判断できない。結果として変更を恐れ、技術的負債が積み上がる。
ADRは、この問題に対する最もシンプルな解決策だ。Markdownで書く。Gitで管理する。コードと一緒にレビューする。それだけ。
ADRの構造: 4つのセクション
ADRは、以下の4つのセクションで構成される。これはNygardが提案したオリジナル形式だ。
# ADR-001: [タイトル]
## Status(ステータス)
Proposed | Accepted | Deprecated | Superseded by ADR-XXX
## Context(コンテキスト)
この決定が必要になった背景と、直面していた問題を書く。
技術的な状況、ビジネス上の要件、チームの制約など。
## Decision(決定)
どう決めたか。能動態で書く。
「〜にする」「〜を採用する」
## Consequences(結果・影響)
この決定によってどうなるか。
メリットとデメリット、将来への影響を正直に書く。各セクションの役割を理解することが重要だ。
Status: このADRの現在の状態。Proposedは議論中、Acceptedは決定済み、Supersededは新しいADRに置き換えられた状態。
Context: 「その時点で何が起きていたか」。技術的な状況、制約、要件。現在形・過去形で書く。
Decision: 「何を決めたか」。一文で明確に。能動態で書く。
Consequences: 「この決定で何が変わるか」。良い面も悪い面も正直に書く。
INFO
ADRは「正しい判断を記録する」のではなく、「その時点での最善の判断と、その理由を記録する」ものだ。後から見て間違っていても、それ自体が重要な情報になる。「当時はそう判断した理由があった」と分かることで、変更の意思決定が正確になる。
最初のADR: Railsのバージョン選定
シンジはチームに最初のADRのサンプルを見せた。過去に誰かが「なんとなく」決めていたRailsのバージョン選定を、今の知識で書き直したものだ。
# ADR-001: Ruby on Railsをメインフレームワークとして採用する
## Status
Accepted
## Context
2022年3月、新規Webアプリケーションのフレームワーク選定が必要になった。
チームの技術スタック:
- バックエンドエンジニア: 5名(Ruby経験者3名、Python経験者2名)
- プロダクトのコア: CRUD操作が多いB2B SaaS
- 優先事項: 開発速度、採用市場でのエンジニア確保
検討したフレームワーク:
- Ruby on Rails 7.x
- FastAPI (Python)
- Laravel (PHP)
## Decision
Ruby on Rails 7.xを採用する。
理由:
1. チームの過半数(3/5)がRuby経験者であり、学習コストが最小
2. ActiveRecordによるDB操作の簡素化が、CRUD中心のSaaSに適合
3. Convention over Configurationにより、小チームでも一貫したコードベースを維持しやすい
4. Hotwireの登場によりフロントエンド負荷を減らせる
5. 採用市場でRubyエンジニアが確保しやすい(東京圏で判断)
FastAPIを選ばなかった理由:
- Python経験者は2名のみ。3名の再学習コストが高い
- ORM(SQLAlchemy)の設定が複雑でCRUD中心SaaSには過剰
Laravelを選ばなかった理由:
- PHP経験者がチームにいない
- 採用時のPHPエンジニア確保が難しいと判断
## Consequences
良い影響:
- 既存エンジニアが即戦力として機能
- Rails wayに従うことでコードレビューのコストが下がる
- scaffold, migration等のツールで開発速度が上がる
悪い影響・リスク:
- PythonエンジニアはRubyの学習期間(1-2ヶ月)が必要
- スレッドモデルではないため、大量の同時接続には注意が必要
- Python製の機械学習ライブラリを組み込む場合はAPI経由での連携が必要
再評価のトリガー:
- チームの組成が大きく変わった場合
- パフォーマンス要件がRailsのキャパシティを超えた場合マイが読み終えて言った。「確かにこれ、最初からあれば3年後の俺たちが困らなかったね」
「そう。これを今書くのは遡及的だけど、それでも意味がある。なぜRailsを使っているかを、このチームの全員が参照できるようになる」
「でも、当時の判断者がいないのに書いていいの?」とリョウ。
「書けるだけ書けばいい。不完全でも、ゼロよりまし。不明な部分は『当時の担当者不在のため不明』と書けばいい。それすら情報だ」
ADRのメリット: 4つの価値
1. 未来の自分への手紙
コードを書いた6ヶ月後、そのコードを変更しなければならない状況になる。なぜそう書いたか、覚えていない。ADRはその答えだ。
# このコードを変更する前に、必ずADR-023を読むこと
# docs/adr/023-payment-retry-logic.md
#
# 要点: RETRY_INTERVALが指数バックオフでなく固定値な理由は
# 決済APIプロバイダーのSLAが30秒インターバルの再試行を推奨しているため
class PaymentService
MAX_RETRY = 3
RETRY_INTERVAL = [1, 5, 30].freeze
def charge_with_retry(amount, card_token)
RETRY_INTERVAL.each_with_index do |wait, attempt|
result = stripe_charge(amount, card_token)
return result if result.success?
Rails.logger.warn("Payment attempt #{attempt + 1} failed: #{result.error}")
sleep(wait) unless attempt == MAX_RETRY - 1
end
raise PaymentFailedError, "#{MAX_RETRY}回の試行後も失敗"
end
private
def stripe_charge(amount, card_token)
Stripe::Charge.create(
amount: amount,
currency: "jpy",
source: card_token
)
rescue Stripe::CardError => e
OpenStruct.new(success?: false, error: e.message)
end
endADRのコメントがあることで、「なぜ指数バックオフでなく固定値なのか」を調べる時間がゼロになる。そしてうっかり「効率化」のために指数バックオフに変えてしまう失敗も防げる。
2. 新メンバーのオンボーディング加速
ADRのディレクトリを読むだけで、プロダクトの「設計思想の歴史」が分かる。なぜこのアーキテクチャになっているか、何を捨てて何を取ったかが見える。
ユウキのような新メンバーは、「コードが読める」から「なぜそのコードなのか分かる」へと、理解のレベルが上がる。
シンジが実際に測定したデータがある。ADR導入前、新メンバーが「独立して判断を下せるようになるまで」の期間が平均3ヶ月だった。ADR導入後、その期間は6週間になった。
3. 議論の質が上がる
「何となくAWSのほうが良さそう」という議論が、「前回ADR-007でGCPを選んだ。理由は〇〇だった。今回その条件は変わったか?」という議論になる。
感情論ではなく、事実と根拠に基づいた議論ができる。
また、「同じ議論を何度もしない」という効果もある。ADRが一つの判断のアンカーになり、同じ技術を毎回ゼロから比較する無駄がなくなる。
# チームSlackの会話(ADR前)
マイ: 「今度の機能、どのgemを使う?」
リョウ: 「kaminariかwill_paginate?」
コウスケ: 「どっちが良かったっけ」
マイ: 「前も同じ話したよね。またゼロから調べる?」
# 30分の議論の末、以前と同じ結論に達する
# チームSlackの会話(ADR後)
マイ: 「今度の機能、ページネーションどうする?」
リョウ: 「ADR-021にkaminari選んだ理由が書いてある。継続でいい?」
コウスケ: 「条件変わってないね。kaminariで行こう」
# 3分で完了4. 「なぜ変えるか」も記録できる
ADRは最初の判断だけでなく、その判断を覆すときにも使う。
# ADR-023のStatus更新
## Status
Superseded by ADR-045
ADR-023でResqueを採用したが、ADR-045でSidekiqに移行する。
移行の理由は ADR-045 を参照。
移行タイムライン: 2024年Q2
移行担当: リョウ古いADRが無効になっても、削除せず「Superseded」にする。歴史が残る。なぜ変えたかも分かる。
INFO
ADRの最大の価値は、「正しい判断を記録すること」ではなく、「判断のコンテキストを保存すること」にある。間違った判断を残すことも、将来への重要な情報だ。「当時はこう判断したが、条件が変わった」という変化の記録こそがチームの成長を示す。
ADRのファイル構成
シンジがチームに提案したディレクトリ構成はシンプルだった。
my-rails-app/
├── app/
├── config/
├── db/
├── docs/
│ └── adr/
│ ├── README.md # ADR一覧(インデックス)
│ ├── 001-rails-framework.md
│ ├── 002-postgresql.md
│ ├── 003-sidekiq-background-jobs.md
│ ├── templates/
│ │ └── adr-template.md # テンプレート
│ └── ...
├── spec/
└── README.md
ポイントは3つ。
- 連番でナンバリング:
001,002,003。検索しやすく、順番が分かる - 削除しない: 廃止されたADRも残す。歴史の記録だから
- 小さく始める: 完璧なフォーマットより、書き続けることが重要
ステータスの種類と意味
ADRのステータスフィールドは、意思決定のライフサイクルを表す。
| ステータス | 意味 | 使うタイミング |
|---|---|---|
Proposed | 提案中 | PRを出してレビュー待ちのとき |
Accepted | 承認済み | チームが合意してマージしたとき |
Deprecated | 非推奨 | その判断が今は適用されないとき |
Superseded by ADR-XXX | 上書きされた | 新しいADRで置き換えられたとき |
Rejected | 却下 | 提案したが採用しなかったとき |
重要なのは Rejected も残すことだ。「なぜこの案を採用しなかったか」は、将来同じ案が提案されたときの参考になる。
ADRは「重い」ドキュメントではない
コウスケが手を挙げた。「毎回ADR書くの、重くないですか?」
「軽くするのが大事」とシンジは答えた。
ADRを書くべき意思決定と、書かなくていい判断がある。
ADRを書くべき判断:
- フレームワーク・言語の選定
- データベース・ストレージの選定
- 認証・認可の方式
- インフラ構成の大きな変更
- マイクロサービス化などのアーキテクチャ変更
- パフォーマンス・セキュリティに関わる設計判断
- 外部サービス・API連携の選定
ADRを書かなくていい判断:
- コーディングスタイル(linterに任せる)
- 変数名・関数名の命名
- 小さなバグ修正の方針
- 一時的なワークアラウンド(ただし一時的であることはコメントに書く)
- テストの具体的な書き方
WARNING
全ての判断をADRにしようとすると破綻する。「6ヶ月後に自分が『なぜ?』と思いそうか」を基準にする。そう思いそうなものだけ書けばいい。週に1〜2本書ければ十分だ。
ミニADRという選択肢
重要な判断でも、長いADRを書く必要はない。「ミニADR」として3〜5行でも十分な場合がある。
# ADR-021: ページネーションにKaminariを使う
## Status
Accepted
## Context
一覧ページのページネーション実装が必要。will_paginateとKaminariが候補。
## Decision
Kaminariを採用する。チームの過去プロジェクトでの使用実績があり、
Railsの新バージョンへの対応が継続的に行われているため。
また will_paginate は Rails 7 との互換性に不安がある。
## Consequences
チームの学習コストがゼロ。will_paginateへの移行は基本的に不要。
将来的に100万件を超えるページネーションが必要になった場合は
cursor-based paginationへの移行を検討する。「これ、5分で書ける」とユウキが言った。
「そう。ADRは論文じゃない。判断の記録だ。5分で書けるなら、書かない理由がない」
コードベースとADRの対応
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のConsequences節に根拠が記載されている
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# app/models/user.rb
#
# JWT認証の設計については ADR-012 を参照:
# docs/adr/012-jwt-refresh-token-auth.md
#
class User < ApplicationRecord
has_secure_password
has_many :refresh_tokens, dependent: :destroy
# plan_typeのlegacy扱いについてはADR-001のContextを参照
LEGACY_PLAN_TYPES = %w[legacy_v1 enterprise_old].freeze
def legacy_plan?
LEGACY_PLAN_TYPES.include?(plan_type)
end
endコードとADRが紐づくことで、「このコードを変えようとしたらADRを読む必然性が生まれる」という状態になる。
ADRとコードの対応関係
ADRはコードと独立したドキュメントではない。コードと一対一で対応する。
docs/adr/ app/
├── 001-rails-framework.md ← Gemfile, config/application.rb
├── 002-redis-cache.md ← config/initializers/cache_store.rb
├── 008-postgresql.md ← config/database.yml, db/schema.rb
├── 012-jwt-auth.md ← app/controllers/application_controller.rb
└── 015-ecs-fargate.md ← Dockerfile, .github/workflows/deploy.yml
これを「コードとADRの対応マップ」と呼ぶ。コードを変えるとき、対応するADRを更新する(または新しいADRを書く)。
Railsでは、このマッピングをコメントで明示することで、コードリーディング中にADRへ誘導できる:
# config/initializers/cache_store.rb
# キャッシュストアの選定: ADR-002 参照
# docs/adr/002-redis-cache.md
# 選定理由: ElastiCache Redisを採用(Memcachedではなく)
Rails.application.configure do
config.cache_store = :redis_cache_store, {
url: ENV.fetch("REDIS_URL"),
expires_in: 15.minutes
}
end# app/controllers/api/v1/base_controller.rb
# JWT認証の設計: ADR-012 参照
# docs/adr/012-jwt-refresh-token-auth.md
# 注意: アクセストークンはhttpOnly Cookie推奨(localStorageは禁止)
class Api::V1::BaseController < ApplicationController
before_action :authenticate_via_jwt!
private
def authenticate_via_jwt!
token = request.headers["Authorization"]&.split(" ")&.last
# 実装の詳細はJwtServiceを参照
payload = JwtService.decode(token)
@current_user = User.find(payload[:user_id])
rescue AuthError => e
render json: { error: e.message }, status: :unauthorized
end
endこの「コード ↔ ADR の双方向参照」が、コードとドキュメントの乖離を防ぐ最も効果的な方法だ。
既存プロジェクトへのADR導入
「でも、うちは既存のプロジェクトだよ。今さら過去の判断を書き起こせる?」とマイ。
「書けるだけ書けばいい」とシンジ。「遡及ADRは不完全でも意味がある」
遡及ADRの書き方:
# ADR-001: MySQL 5.7を採用している(遡及的記録)
## Status
Accepted(ただしADR-008で置き換えの議論中)
## Context
(当時の詳細は不明。以下は現在分かっている範囲)
2021年頃、データベース選定が行われた。
当時の担当者は既に退職しており、詳細な判断経緯は不明。
現在のコードベースからの推測: CRUD中心のシンプルなスキーマが多いため、
MySQLの標準的な機能で十分だったと思われる。
## Decision
MySQL 5.7を採用した。
## Consequences
(現時点での評価)
- 現在の要件(CRUD中心のSaaS)には十分に対応できている
- 位置情報機能の追加要件(ADR-008参照)ではPostGISの優位性が明らか
- MySQL 5.7はEOLが近づいており、バージョンアップまたは移行が必要
## 注記
このADRは遡及的に作成されたものであり、当時の判断者・判断根拠が
完全には復元できていない。より詳細な経緯を知っている方はコメントで補足を。「不完全でも書く」という姿勢が、ADR文化を始める第一歩だ。
シンジの確信
ミーティングの最後、シンジはこう締めくくった。
「ADRは、今日の判断を未来の自分たちへの贈り物にする仕組みだ。コードは動く理由を説明しない。ADRが動く理由を説明する」
マイは少し考えてから言った。「……試してみる価値はあるかもしれない」
「じゃあ、まず5本書いてみよう」とシンジは提案した。「過去の重要な判断から。今週中に。俺も書く。5本揃ったらチームで読み合わせしよう」
チームは翌週から、まず過去の重要な判断を5つ、ADRとして書き起こすことにした。
遡及するADRは不完全かもしれない。でも「書かないより書いたほうがいい」。ADRの文化は、ゼロから始まらない。一つ目のADRから始まる。
次の章では、実際にADRを書く方法を、テンプレートと実例を使って学ぶ。良いADRと悪いADRの違い、そして「書けるようになる」ための具体的な指針を示す。