ADR とは何か・なぜ必要か

ADR(Architecture Decision Record)は、ソフトウェアシステムに影響を与える重要な技術的意思決定を記録したドキュメントだ。 1つの ADR は1つの決定を記録し、「何を決めたか」だけでなく「なぜその選択をしたか」「何を犠牲にしたか」まで含める点が通常の設計文書と異なる。

ADR が解決する問題は明確だ。開発チームは日々多くのアーキテクチャ決定を行うが、その多くが口頭やチャットで完結し記録が残らない。 数ヶ月後に「なぜ PostgreSQL ではなく MongoDB を選んだのか」「あの API の認証方式はどういう理由で JWT にしたのか」という問いに答えられる人間がいなくなる。 ADR はこの「知識の消失」を防ぐための軽量な記録実践だ。

💡 ADR の3原則

軽量:1ページ以内の Markdown ファイルで完結させる。詳細な設計文書の代替ではない。

不変:決定を記録した ADR は変更しない。決定が変わったら新しい ADR を作成し、古い ADR を Deprecated にする。

コードと同居:ADR はリポジトリの docs/adr/ に配置してコードと一緒にバージョン管理する。

ADR の5つの構成要素

ADR はシンプルな5つのセクションで構成される。各セクションに何を書くかを明確に定義することが、チーム全体での統一された記録実践の第一歩だ。

セクション記載内容記述のポイント
タイトル + 番号 決定を一文で表すタイトルと連番(ADR-0023 など) 「〇〇を✕✕で行う」という形式が望ましい。番号は固定で変更しない
ステータス 現在の状態(Proposed / Accepted / Rejected / Deprecated) リンク付きで「ADR-0025 によって Deprecated」のように関連ADRを参照する
背景(Context) この決定が必要になった理由・状況・制約 「なぜ今この問題を解決する必要があったか」を将来の読者にわかるように書く
決定事項(Decision) 何を選択したか。複数の選択肢と選んだ理由 採用した選択肢だけでなく、検討したが採用しなかった選択肢とその理由も記載する
トレードオフ(Consequences) この決定によって得たものと失ったもの ポジティブな結果だけでなく、将来の開発者が認識すべき制約・負債も明記する

⚠️ 背景(Context)を最も丁寧に書く

多くの ADR が形骸化する原因は、Context が「〇〇の実装が必要だった」という1行で終わることだ。Context には当時の技術的制約・チームの規模・パフォーマンス要件・採用を検討したが却下した選択肢も含めるべきだ。将来の開発者が「あのとき PostgreSQL ではなく MongoDB を選んだのは、当時のチームに ORM の知見がなかったからだ」と理解できる記録が理想だ。

ステータスライフサイクル

ADR のステータスは4段階で管理される。それぞれのステータスの意味と遷移ルールを定義する。

ステータス意味遷移先
Proposed 提案中。PR としてレビューを依頼した状態。まだ採用決定していない Accepted / Rejected
Accepted チームで合意し、採用が決定した状態。コードに実装される Deprecated(決定が覆ったとき)
Rejected レビューで却下された状態。記録として残すが実装されない (終端)
Deprecated 一度 Accepted になったが、後継の決定(別の ADR)に置き換えられた状態 (終端)

💡 Rejected の ADR も削除せず残す

却下された選択肢がなぜ却下されたかは、未来に同じ議論が再燃したときに「過去に同じことを検討し、△△という理由で採用しなかった」と示す証拠になる。Rejected の ADR はそのままリポジトリに残し、Index に掲載する。

ADR 構造フロー図

ADR のファイル構成と Proposed から Accepted/Rejected、そして Deprecated へと遷移するライフサイクルを示す。

ADR 構造とステータスライフサイクル

図1:ADR の5つの構成要素(左)とステータスライフサイクル(右)

ファイル命名規則とディレクトリ配置

ADR ファイルは docs/adr/ ディレクトリにゼロパディングした連番 + タイトルのスラッグで命名する。 番号は一度付けたら変更しない。

Shell — ディレクトリ構造の例
docs/
└── adr/
    ├── 0001-use-postgresql-for-primary-database.md
    ├── 0002-use-jwt-for-api-authentication.md
    ├── 0003-adopt-monorepo-with-turborepo.md
    ├── 0004-use-mongodb-for-event-log-storage.md   # Rejected
    ├── 0005-use-redis-for-session-storage.md
    └── 0023-migrate-auth-to-oauth2.md              # Deprecated → 0030 参照

ADR テンプレート

チームで統一して使える ADR テンプレートを示す。リポジトリの docs/adr/TEMPLATE.md として配置しておく。

Markdown — docs/adr/TEMPLATE.md
# ADR-XXXX: [決定を一文で表すタイトル]

## ステータス

Proposed 

## 背景(Context)



## 決定事項(Decision)



## トレードオフ(Consequences)


      

Python 一覧管理スクリプト

ADR ファイルを自動で読み込み、ステータス別の一覧と README.md の Index を生成するスクリプトだ。 ADR の数が増えてきたとき、手動で Index を管理するのは現実的でないため自動化する。

Python — list_adr.py
"""
ADR 一覧管理スクリプト
docs/adr/ 以下の .md ファイルを読み込み、
ステータス別の一覧表示と README.md の Index を生成する。
"""

import re
import sys
from pathlib import Path
from dataclasses import dataclass

VALID_STATUSES = {"Proposed", "Accepted", "Rejected", "Deprecated"}
ADR_NUMBER_PATTERN = re.compile(r"^(\d{4})-")
STATUS_PATTERN = re.compile(r"^(?:Proposed|Accepted|Rejected|Deprecated)", re.MULTILINE)
TITLE_PATTERN = re.compile(r"^# ADR-\d+[::\s]+(.+)$", re.MULTILINE)


@dataclass
class ADREntry:
    number: str
    title: str
    status: str
    filepath: Path

    @property
    def status_badge(self) -> str:
        badges = {
            "Proposed": "🟡",
            "Accepted": "✅",
            "Rejected": "❌",
            "Deprecated": "⚫",
        }
        return badges.get(self.status, "❓")


def parse_adr(path: Path) -> ADREntry | None:
    """ADR ファイルをパースして ADREntry を返す"""
    m = ADR_NUMBER_PATTERN.match(path.stem)
    if not m:
        return None

    number = m.group(1)
    content = path.read_text(encoding="utf-8")

    title_m = TITLE_PATTERN.search(content)
    title = title_m.group(1).strip() if title_m else path.stem

    status_m = STATUS_PATTERN.search(content)
    status = status_m.group(0).strip() if status_m else "Unknown"

    return ADREntry(number=number, title=title, status=status, filepath=path)


def list_adrs(adr_dir: str = "docs/adr") -> list[ADREntry]:
    """ADR ディレクトリを走査してエントリのリストを返す"""
    adr_path = Path(adr_dir)
    if not adr_path.exists():
        print(f"❌ ディレクトリが見つかりません: {adr_dir}")
        return []

    entries = []
    for md_file in sorted(adr_path.glob("*.md")):
        if md_file.name == "TEMPLATE.md" or md_file.name == "README.md":
            continue
        entry = parse_adr(md_file)
        if entry:
            entries.append(entry)

    return entries


def print_summary(entries: list[ADREntry]) -> None:
    """ステータス別に ADR 一覧を出力する"""
    by_status: dict[str, list[ADREntry]] = {s: [] for s in VALID_STATUSES}
    for entry in entries:
        by_status.setdefault(entry.status, []).append(entry)

    for status in ["Accepted", "Proposed", "Rejected", "Deprecated"]:
        group = by_status.get(status, [])
        if not group:
            continue
        print(f"\n{group[0].status_badge} {status} ({len(group)}件)")
        for e in group:
            print(f"  ADR-{e.number}: {e.title}")


def generate_index(entries: list[ADREntry]) -> str:
    """README.md 用の ADR インデックス Markdown を生成する"""
    lines = ["# ADR Index\n"]
    for status in ["Accepted", "Proposed", "Rejected", "Deprecated"]:
        group = [e for e in entries if e.status == status]
        if not group:
            continue
        emoji = {"Accepted": "✅", "Proposed": "🟡", "Rejected": "❌", "Deprecated": "⚫"}
        lines.append(f"\n## {emoji[status]} {status}\n")
        for e in group:
            rel = e.filepath.name
            lines.append(f"- [ADR-{e.number}: {e.title}]({rel})")
    return "\n".join(lines)


def main() -> int:
    adr_dir = sys.argv[1] if len(sys.argv) > 1 else "docs/adr"
    entries = list_adrs(adr_dir)

    if not entries:
        print("ADR ファイルが見つかりませんでした。")
        return 0

    print(f"ADR 合計: {len(entries)} 件")
    print_summary(entries)

    # README.md を生成
    readme_path = Path(adr_dir) / "README.md"
    readme_path.write_text(generate_index(entries), encoding="utf-8")
    print(f"\n✅ インデックスを生成しました: {readme_path}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

実行例と出力を示す。

Shell — 実行例
$ python tools/list_adr.py docs/adr
ADR 合計: 6 件

✅ Accepted (4件)
  ADR-0001: PostgreSQL をプライマリデータベースとして採用する
  ADR-0002: API 認証に JWT を使用する
  ADR-0003: Turborepo によるモノレポを採用する
  ADR-0005: セッションストレージに Redis を使用する

🟡 Proposed (1件)
  ADR-0023: OAuth2 への認証移行

❌ Rejected (1件)
  ADR-0004: イベントログに MongoDB を使用する

✅ インデックスを生成しました: docs/adr/README.md

いつ ADR を書くか

ADR を書くタイミングの判断基準を以下に示す。すべての技術的決定を記録する必要はなく、「後から理由を聞かれる可能性がある決定」を対象にする。

ADR を書くべき決定ADR が不要な決定
データベースエンジンの選択・変更 変数名・関数名などの命名
認証・認可方式の選択 コードフォーマッターの設定値
API 設計の方針(REST vs GraphQL) パッケージのマイナーバージョン更新
インフラアーキテクチャ(モノリス vs マイクロサービス) スタイルシートの色定義
外部サービスとのインテグレーション方式 ユニットテストの追加
モノレポ・ポリレポの選択 コメントの追記

「30分以上議論したら ADR を書く」ルール

議論の長さは重要性の指標になる。30分以上チームで議論した技術的決定は、記録する価値がある。議論の結論だけでなく、議論の過程で出た反論・懸念・採用しなかった選択肢も ADR の Context / Consequences に含める。

まとめ

ADR はチームの「技術的記憶」を体系化する軽量なドキュメント実践だ。以下の4点が運用の核心だ。

  • 5つの構成要素:タイトル / ステータス / 背景(Context)/ 決定事項(Decision)/ トレードオフ(Consequences)
  • ステータスライフサイクル:Proposed → Accepted/Rejected → Deprecated。ADR は上書きせず新規作成で記録を積み上げる
  • コードとの同居docs/adr/ に配置してバージョン管理に含め、PR レビューで合意形成する
  • Python 自動管理:一覧表示・README Index 自動生成でメンテナンスコストを下げる

リリース時の ADR の役割

新機能リリース時に「このリリースで採用した技術的決定はすでに ADR として記録されているか」を確認する習慣をつける。重要な設計決定が ADR なしにコードに混入した場合は、リリース後でも遡って記録する。ADR が蓄積されることで、新メンバーのオンボーディングコストが大幅に下がる。

参考・公式ドキュメント